@adecore/editor-react 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CallHierarchyPanel.d.ts +6 -0
- package/dist/CallHierarchyPanel.js +88 -0
- package/dist/CompletionPopup.js +11 -3
- package/dist/DiffView.d.ts +22 -0
- package/dist/DiffView.js +203 -0
- package/dist/EditorContextMenu.js +1 -0
- package/dist/EditorView.js +18 -1
- package/dist/FindReplace.d.ts +1 -1
- package/dist/FindReplace.js +41 -11
- package/dist/HoverCard.js +2 -2
- package/dist/LanguagePopups.js +4 -1
- package/dist/Markdown.d.ts +2 -1
- package/dist/Markdown.js +8 -4
- package/dist/PeekPanel.d.ts +3 -0
- package/dist/PeekPanel.js +2 -3
- package/dist/PickPopup.js +15 -2
- package/dist/RenameCard.js +10 -5
- package/dist/RenderedDocs.d.ts +9 -0
- package/dist/RenderedDocs.js +84 -0
- package/dist/SplitDiffView.d.ts +9 -0
- package/dist/SplitDiffView.js +276 -0
- package/dist/SymbolPicker.d.ts +3 -0
- package/dist/SymbolPicker.js +4 -3
- package/dist/call-hierarchy-model.d.ts +52 -0
- package/dist/call-hierarchy-model.js +99 -0
- package/dist/call-hierarchy.d.ts +42 -0
- package/dist/call-hierarchy.js +399 -0
- package/dist/code-actions-model.d.ts +2 -0
- package/dist/code-actions-model.js +12 -10
- package/dist/code-actions.js +3 -1
- package/dist/completion-model.d.ts +14 -4
- package/dist/completion-model.js +40 -33
- package/dist/completion.d.ts +4 -0
- package/dist/completion.js +257 -42
- package/dist/diagnostics.js +8 -6
- package/dist/diff-expansion.d.ts +9 -0
- package/dist/diff-expansion.js +41 -0
- package/dist/diff-overlays.d.ts +15 -0
- package/dist/diff-overlays.js +28 -0
- package/dist/diff-syntax.d.ts +16 -0
- package/dist/diff-syntax.js +49 -0
- package/dist/diff-wordmarks.d.ts +5 -0
- package/dist/diff-wordmarks.js +26 -0
- package/dist/doc-comment-model.d.ts +23 -0
- package/dist/doc-comment-model.js +291 -0
- package/dist/editor-language.d.ts +13 -1
- package/dist/editor-language.js +14 -4
- package/dist/editor-react.css +79 -0
- package/dist/format-on-type.d.ts +14 -0
- package/dist/format-on-type.js +96 -0
- package/dist/host-types.d.ts +17 -0
- package/dist/hover.d.ts +4 -0
- package/dist/hover.js +15 -2
- package/dist/index.d.ts +19 -5
- package/dist/index.js +9 -1
- package/dist/inlay-hints.d.ts +47 -2
- package/dist/inlay-hints.js +353 -16
- package/dist/language-ids.js +4 -2
- package/dist/locales/en.json +114 -3
- package/dist/locales/nl.json +114 -3
- package/dist/models.d.ts +6 -2
- package/dist/models.js +5 -1
- package/dist/patch-decorations.d.ts +27 -0
- package/dist/patch-decorations.js +182 -0
- package/dist/patch-layout.d.ts +40 -0
- package/dist/patch-layout.js +304 -0
- package/dist/peek.js +1 -0
- package/dist/pick.d.ts +1 -0
- package/dist/pick.js +11 -1
- package/dist/popups.d.ts +17 -0
- package/dist/popups.js +1 -0
- package/dist/project-language.d.ts +8 -0
- package/dist/project-language.js +34 -2
- package/dist/refresher.d.ts +1 -0
- package/dist/refresher.js +5 -0
- package/dist/rename-model.d.ts +1 -1
- package/dist/rename-model.js +3 -7
- package/dist/rename.js +5 -8
- package/dist/rendered-docs.d.ts +62 -0
- package/dist/rendered-docs.js +368 -0
- package/dist/row-host.d.ts +2 -0
- package/dist/row-host.js +11 -3
- package/dist/semantic-delta.d.ts +2 -0
- package/dist/semantic-delta.js +27 -0
- package/dist/semantic-model.js +16 -0
- package/dist/semantic-tokens.d.ts +26 -1
- package/dist/semantic-tokens.js +174 -14
- package/dist/shortcut-keys.d.ts +2 -0
- package/dist/shortcut-keys.js +6 -2
- package/dist/signature.js +1 -1
- package/dist/snippet-field-transforms.d.ts +30 -0
- package/dist/snippet-field-transforms.js +116 -0
- package/dist/snippet-indent.d.ts +2 -0
- package/dist/snippet-indent.js +49 -0
- package/dist/snippet-range-order.d.ts +3 -0
- package/dist/snippet-range-order.js +19 -0
- package/dist/snippet-session.d.ts +42 -6
- package/dist/snippet-session.js +650 -53
- package/dist/snippet-transform-client.d.ts +19 -0
- package/dist/snippet-transform-client.js +66 -0
- package/dist/snippet-transform-format.d.ts +15 -0
- package/dist/snippet-transform-format.js +302 -0
- package/dist/snippet-transform-protocol.d.ts +45 -0
- package/dist/snippet-transform-protocol.js +8 -0
- package/dist/snippet-transform-queue.d.ts +32 -0
- package/dist/snippet-transform-queue.js +173 -0
- package/dist/snippet-transform-worker.d.ts +10 -0
- package/dist/snippet-transform-worker.js +41 -0
- package/dist/snippet-variable-names.d.ts +1 -0
- package/dist/snippet-variable-names.js +36 -0
- package/dist/snippet-variables.d.ts +7 -0
- package/dist/snippet-variables.js +66 -0
- package/dist/snippet.d.ts +30 -3
- package/dist/snippet.js +402 -87
- package/dist/split-alignment.d.ts +6 -0
- package/dist/split-alignment.js +38 -0
- package/dist/split-scroll.d.ts +14 -0
- package/dist/split-scroll.js +45 -0
- package/dist/symbols.d.ts +2 -1
- package/dist/symbols.js +39 -7
- package/dist/testing.d.ts +5 -1
- package/dist/testing.js +12 -0
- package/dist/workspace-edit.d.ts +25 -3
- package/dist/workspace-edit.js +282 -143
- package/package.json +10 -5
- package/src/CallHierarchyPanel.tsx +238 -0
- package/src/CompletionPopup.tsx +12 -1
- package/src/DiffView.tsx +262 -0
- package/src/EditorContextMenu.tsx +1 -0
- package/src/EditorView.tsx +21 -2
- package/src/FindReplace.tsx +51 -13
- package/src/HoverCard.tsx +23 -21
- package/src/LanguagePopups.tsx +5 -0
- package/src/Markdown.tsx +10 -3
- package/src/PeekPanel.tsx +2 -3
- package/src/PickPopup.tsx +18 -2
- package/src/RenameCard.tsx +18 -0
- package/src/RenderedDocs.tsx +155 -0
- package/src/SplitDiffView.tsx +337 -0
- package/src/SymbolPicker.tsx +4 -3
- package/src/call-hierarchy-model.ts +158 -0
- package/src/call-hierarchy.ts +441 -0
- package/src/code-actions-model.ts +17 -11
- package/src/code-actions.ts +3 -1
- package/src/completion-model.ts +61 -35
- package/src/completion.ts +302 -42
- package/src/diagnostics.ts +8 -6
- package/src/diff-expansion.ts +49 -0
- package/src/diff-overlays.tsx +69 -0
- package/src/diff-syntax.ts +67 -0
- package/src/diff-wordmarks.ts +31 -0
- package/src/doc-comment-model.ts +336 -0
- package/src/editor-language.ts +21 -5
- package/src/editor-react.css +79 -0
- package/src/format-on-type.ts +111 -0
- package/src/host-types.ts +19 -0
- package/src/hover.ts +18 -3
- package/src/index.ts +27 -4
- package/src/inlay-hints.ts +394 -18
- package/src/language-ids.ts +4 -2
- package/src/locales/en.json +114 -3
- package/src/locales/nl.json +114 -3
- package/src/models.ts +40 -1
- package/src/patch-decorations.ts +224 -0
- package/src/patch-layout.ts +388 -0
- package/src/peek.ts +1 -0
- package/src/pick.ts +12 -1
- package/src/popups.ts +24 -0
- package/src/project-language.ts +43 -2
- package/src/refresher.ts +6 -0
- package/src/rename-model.ts +4 -8
- package/src/rename.ts +5 -8
- package/src/rendered-docs.ts +422 -0
- package/src/row-host.ts +12 -3
- package/src/semantic-delta.ts +29 -0
- package/src/semantic-model.ts +17 -0
- package/src/semantic-tokens.ts +202 -19
- package/src/shortcut-keys.ts +6 -2
- package/src/signature.ts +1 -1
- package/src/snippet-field-transforms.ts +144 -0
- package/src/snippet-indent.ts +53 -0
- package/src/snippet-range-order.ts +19 -0
- package/src/snippet-session.ts +797 -56
- package/src/snippet-transform-client.ts +84 -0
- package/src/snippet-transform-format.ts +325 -0
- package/src/snippet-transform-protocol.ts +24 -0
- package/src/snippet-transform-queue.ts +199 -0
- package/src/snippet-transform-worker.ts +58 -0
- package/src/snippet-variable-names.ts +36 -0
- package/src/snippet-variables.ts +74 -0
- package/src/snippet.ts +452 -89
- package/src/split-alignment.ts +45 -0
- package/src/split-scroll.ts +54 -0
- package/src/symbols.ts +43 -7
- package/src/testing.ts +25 -0
- package/src/workspace-edit.ts +347 -169
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createWorkerRequestClient, WorkerRequestError, type WorkerRequestPort } from '@adecore/editor/worker-client';
|
|
2
|
+
import { validateSnippetTransformJobs } from './snippet-transform-format.ts';
|
|
3
|
+
import { MAX_TRANSFORM_CHARACTERS, type SnippetTransformInput, type SnippetTransformOutput, type SnippetTransformJob } from './snippet-transform-protocol.ts';
|
|
4
|
+
export type SnippetTransformErrorKind = 'syntax' | 'range' | 'error' | 'timeout' | 'superseded' | 'unavailable' | 'disposed';
|
|
5
|
+
export class SnippetTransformError extends Error {
|
|
6
|
+
readonly kind: SnippetTransformErrorKind;
|
|
7
|
+
constructor(kind: SnippetTransformErrorKind, message: string) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.kind = kind;
|
|
10
|
+
this.name = 'SnippetTransformError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export interface SnippetTransformClientOptions {
|
|
14
|
+
readonly workerFactory: () => WorkerRequestPort;
|
|
15
|
+
readonly deadline?: number;
|
|
16
|
+
readonly schedule?: (callback: () => void, delay: number) => () => void;
|
|
17
|
+
}
|
|
18
|
+
export interface SnippetTransformClient {
|
|
19
|
+
transform(jobs: readonly SnippetTransformJob[]): Promise<readonly string[]>;
|
|
20
|
+
cancel(): void;
|
|
21
|
+
release(): void;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
24
|
+
export function createSnippetTransformClient(options: SnippetTransformClientOptions): SnippetTransformClient {
|
|
25
|
+
const transport = createWorkerRequestClient<SnippetTransformInput, Exclude<SnippetTransformOutput, { type: 'ready' }>>({
|
|
26
|
+
...options,
|
|
27
|
+
failure: (reply) =>
|
|
28
|
+
reply.type === 'error' ? { error: new SnippetTransformError(reply.kind, reply.message), restart: reply.kind === 'error' } : undefined
|
|
29
|
+
});
|
|
30
|
+
let disposed = false;
|
|
31
|
+
const failure = (error: unknown): SnippetTransformError =>
|
|
32
|
+
error instanceof SnippetTransformError
|
|
33
|
+
? error
|
|
34
|
+
: error instanceof WorkerRequestError
|
|
35
|
+
? new SnippetTransformError(error.kind, error.message)
|
|
36
|
+
: new SnippetTransformError(
|
|
37
|
+
error instanceof SyntaxError ? 'syntax' : error instanceof RangeError ? 'range' : 'error',
|
|
38
|
+
error instanceof Error ? error.message : String(error)
|
|
39
|
+
);
|
|
40
|
+
return {
|
|
41
|
+
transform(jobs) {
|
|
42
|
+
if (disposed) {
|
|
43
|
+
return Promise.reject(new SnippetTransformError('disposed', 'The snippet transform client was disposed.'));
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(jobs) && jobs.length === 0) {
|
|
46
|
+
return Promise.resolve([]);
|
|
47
|
+
}
|
|
48
|
+
let copied: SnippetTransformJob[];
|
|
49
|
+
try {
|
|
50
|
+
validateSnippetTransformJobs(jobs);
|
|
51
|
+
copied = jobs.map((job) => ({ ...job, format: job.format.map((node) => ({ ...node })) }));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return Promise.reject(failure(error));
|
|
54
|
+
}
|
|
55
|
+
return transport
|
|
56
|
+
.request((id) => ({ type: 'transform', id, jobs: copied }))
|
|
57
|
+
.then((reply) => {
|
|
58
|
+
if (
|
|
59
|
+
reply.type !== 'transformed' ||
|
|
60
|
+
!Array.isArray(reply.values) ||
|
|
61
|
+
reply.values.length !== copied.length ||
|
|
62
|
+
reply.values.some((value) => typeof value !== 'string') ||
|
|
63
|
+
reply.values.reduce((sum, value) => sum + value.length, 0) > MAX_TRANSFORM_CHARACTERS
|
|
64
|
+
) {
|
|
65
|
+
throw new SnippetTransformError('error', 'Invalid snippet transform response.');
|
|
66
|
+
}
|
|
67
|
+
return reply.values;
|
|
68
|
+
})
|
|
69
|
+
.catch((error) => {
|
|
70
|
+
throw failure(error);
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
cancel() {
|
|
74
|
+
transport.cancel();
|
|
75
|
+
},
|
|
76
|
+
release() {
|
|
77
|
+
transport.release();
|
|
78
|
+
},
|
|
79
|
+
dispose() {
|
|
80
|
+
disposed = true;
|
|
81
|
+
transport.dispose();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_TRANSFORM_FORMAT,
|
|
3
|
+
MAX_TRANSFORM_NODES,
|
|
4
|
+
MAX_TRANSFORM_PATTERN,
|
|
5
|
+
MAX_TRANSFORM_JOBS,
|
|
6
|
+
MAX_TRANSFORM_VALUE,
|
|
7
|
+
MAX_TRANSFORM_CHARACTERS,
|
|
8
|
+
MAX_TRANSFORM_MATCHES,
|
|
9
|
+
MAX_TRANSFORM_WORK,
|
|
10
|
+
type SnippetTransformCase,
|
|
11
|
+
type SnippetTransformFormatNode,
|
|
12
|
+
type SnippetTransformJob
|
|
13
|
+
} from './snippet-transform-protocol.ts';
|
|
14
|
+
const CASES: ReadonlySet<string> = new Set(['upcase', 'downcase', 'capitalize', 'camelcase', 'pascalcase']);
|
|
15
|
+
function readFormat(source: string, start: number, stopAtSlash: boolean): { nodes: readonly SnippetTransformFormatNode[]; end: number } {
|
|
16
|
+
const nodes: SnippetTransformFormatNode[] = [];
|
|
17
|
+
let at = start,
|
|
18
|
+
text = '';
|
|
19
|
+
const bound = (): void => {
|
|
20
|
+
if (at - start > MAX_TRANSFORM_FORMAT) {
|
|
21
|
+
throw new RangeError('The transform format is too long.');
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const push = (node: SnippetTransformFormatNode): void => {
|
|
25
|
+
if (nodes.length >= MAX_TRANSFORM_NODES) {
|
|
26
|
+
throw new RangeError('The transform format has too many nodes.');
|
|
27
|
+
}
|
|
28
|
+
nodes.push(node);
|
|
29
|
+
};
|
|
30
|
+
const flush = (): void => {
|
|
31
|
+
if (text !== '') {
|
|
32
|
+
push({ kind: 'text', text });
|
|
33
|
+
text = '';
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const string = (colon = false): string => {
|
|
37
|
+
let value = '';
|
|
38
|
+
while (at < source.length && source[at] !== '}' && !(colon && source[at] === ':')) {
|
|
39
|
+
if (source[at] === '\\') {
|
|
40
|
+
if (!['$', '}', '\\'].includes(source[at + 1] ?? '')) {
|
|
41
|
+
throw new SyntaxError('Invalid conditional transform escape.');
|
|
42
|
+
}
|
|
43
|
+
at++;
|
|
44
|
+
}
|
|
45
|
+
value += source[at++]!;
|
|
46
|
+
bound();
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
};
|
|
50
|
+
while (at < source.length && !(stopAtSlash && source[at] === '/')) {
|
|
51
|
+
bound();
|
|
52
|
+
if (source[at] === '\\' && (source[at + 1] === '\\' || source[at + 1] === '/')) {
|
|
53
|
+
at++;
|
|
54
|
+
text += source[at++]!;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (source[at] !== '$') {
|
|
58
|
+
text += source[at++]!;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
at++;
|
|
62
|
+
const brace = source[at] === '{';
|
|
63
|
+
if (brace) {
|
|
64
|
+
at++;
|
|
65
|
+
}
|
|
66
|
+
const start = at;
|
|
67
|
+
while (at < source.length && source.charCodeAt(at) >= 48 && source.charCodeAt(at) <= 57) {
|
|
68
|
+
at++;
|
|
69
|
+
}
|
|
70
|
+
if (at === start) {
|
|
71
|
+
if (brace) {
|
|
72
|
+
throw new SyntaxError('A transform format needs a capture index.');
|
|
73
|
+
}
|
|
74
|
+
text += '$';
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const index = Number(source.slice(start, at));
|
|
78
|
+
if (index > 99) {
|
|
79
|
+
throw new RangeError('Transform captures are limited to 99.');
|
|
80
|
+
}
|
|
81
|
+
flush();
|
|
82
|
+
let node: SnippetTransformFormatNode = { kind: 'capture', index };
|
|
83
|
+
if (brace) {
|
|
84
|
+
if (source[at] === ':') {
|
|
85
|
+
at++;
|
|
86
|
+
const operator = source[at];
|
|
87
|
+
if (operator === '/') {
|
|
88
|
+
at++;
|
|
89
|
+
const operation = string();
|
|
90
|
+
if (!CASES.has(operation)) {
|
|
91
|
+
throw new SyntaxError('Unknown transform case operation.');
|
|
92
|
+
}
|
|
93
|
+
node = { kind: 'capture', index, case: operation as SnippetTransformCase };
|
|
94
|
+
} else if (operator === '+') {
|
|
95
|
+
at++;
|
|
96
|
+
node = { kind: 'conditional', index, if: string() };
|
|
97
|
+
} else if (operator === '?') {
|
|
98
|
+
at++;
|
|
99
|
+
const when = string(true);
|
|
100
|
+
if (source[at++] !== ':') {
|
|
101
|
+
throw new SyntaxError('A conditional transform needs both branches.');
|
|
102
|
+
}
|
|
103
|
+
node = { kind: 'conditional', index, if: when, else: string() };
|
|
104
|
+
} else {
|
|
105
|
+
if (operator === '-') {
|
|
106
|
+
at++;
|
|
107
|
+
}
|
|
108
|
+
node = { kind: 'conditional', index, else: string() };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (source[at++] !== '}') {
|
|
112
|
+
throw new SyntaxError('An unclosed transform capture cannot be applied.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
push(node);
|
|
116
|
+
}
|
|
117
|
+
flush();
|
|
118
|
+
bound();
|
|
119
|
+
if (stopAtSlash && source[at] !== '/') {
|
|
120
|
+
throw new SyntaxError('Unclosed snippet transform format.');
|
|
121
|
+
}
|
|
122
|
+
return { nodes, end: at };
|
|
123
|
+
}
|
|
124
|
+
export function compileSnippetTransformFormat(source: string): readonly SnippetTransformFormatNode[] {
|
|
125
|
+
return readFormat(source, 0, false).nodes;
|
|
126
|
+
}
|
|
127
|
+
export function scanSnippetTransform(
|
|
128
|
+
source: string,
|
|
129
|
+
at: number
|
|
130
|
+
): { pattern: string; flags: string; format: readonly SnippetTransformFormatNode[]; end: number } {
|
|
131
|
+
if (source[at++] !== '/') {
|
|
132
|
+
throw new SyntaxError('A snippet transform needs an opening slash.');
|
|
133
|
+
}
|
|
134
|
+
let pattern = '';
|
|
135
|
+
while (at < source.length && source[at] !== '/') {
|
|
136
|
+
const character = source[at++]!;
|
|
137
|
+
if (character === '\\' && source[at] === '/') {
|
|
138
|
+
pattern += source[at++]!;
|
|
139
|
+
} else {
|
|
140
|
+
pattern += character;
|
|
141
|
+
}
|
|
142
|
+
if (pattern.length > MAX_TRANSFORM_PATTERN) {
|
|
143
|
+
throw new RangeError('The snippet transform pattern is too long.');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (source[at++] !== '/') {
|
|
147
|
+
throw new SyntaxError('Unclosed snippet transform pattern.');
|
|
148
|
+
}
|
|
149
|
+
const parsed = readFormat(source, at, true);
|
|
150
|
+
at = parsed.end + 1;
|
|
151
|
+
let flags = '';
|
|
152
|
+
while (at < source.length && source[at] !== '}') {
|
|
153
|
+
const flag = source[at++]!;
|
|
154
|
+
if (!'gimsu'.includes(flag) || flags.includes(flag)) {
|
|
155
|
+
throw new SyntaxError('Invalid snippet transform flags.');
|
|
156
|
+
}
|
|
157
|
+
flags += flag;
|
|
158
|
+
}
|
|
159
|
+
if (source[at++] !== '}') {
|
|
160
|
+
throw new SyntaxError('Unclosed snippet transform flags.');
|
|
161
|
+
}
|
|
162
|
+
return { pattern, flags, format: parsed.nodes, end: at };
|
|
163
|
+
}
|
|
164
|
+
function capitalize(value: string): string {
|
|
165
|
+
const first = value.slice(0, (value.codePointAt(0) ?? 0) > 0xffff ? 2 : 1);
|
|
166
|
+
return first.toUpperCase() + value.slice(first.length);
|
|
167
|
+
}
|
|
168
|
+
function changeCase(value: string, operation?: SnippetTransformCase): string {
|
|
169
|
+
if (operation === 'upcase') {
|
|
170
|
+
return value.toUpperCase();
|
|
171
|
+
}
|
|
172
|
+
if (operation === 'downcase') {
|
|
173
|
+
return value.toLowerCase();
|
|
174
|
+
}
|
|
175
|
+
if (operation === 'capitalize') {
|
|
176
|
+
return capitalize(value);
|
|
177
|
+
}
|
|
178
|
+
if (operation === 'camelcase' || operation === 'pascalcase') {
|
|
179
|
+
const words = value.match(/[\p{L}0-9]+/gu);
|
|
180
|
+
if (words === null) {
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
return words
|
|
184
|
+
.map((word, index) => (operation === 'camelcase' && index === 0 ? word.slice(0, 1).toLowerCase() + word.slice(1) : capitalize(word)))
|
|
185
|
+
.join('');
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
export function validateSnippetTransformJobs(jobs: readonly SnippetTransformJob[]): void {
|
|
190
|
+
if (!Array.isArray(jobs) || jobs.length > MAX_TRANSFORM_JOBS) {
|
|
191
|
+
throw new RangeError('Too many snippet transform jobs.');
|
|
192
|
+
}
|
|
193
|
+
let characters = 0;
|
|
194
|
+
const patterns = new Set<string>();
|
|
195
|
+
for (const job of jobs) {
|
|
196
|
+
if (
|
|
197
|
+
typeof job.value !== 'string' ||
|
|
198
|
+
job.value.length > MAX_TRANSFORM_VALUE ||
|
|
199
|
+
typeof job.pattern !== 'string' ||
|
|
200
|
+
job.pattern.length > MAX_TRANSFORM_PATTERN
|
|
201
|
+
) {
|
|
202
|
+
throw new RangeError('A snippet transform value or pattern is too large.');
|
|
203
|
+
}
|
|
204
|
+
characters += job.value.length + job.pattern.length;
|
|
205
|
+
if (characters > MAX_TRANSFORM_CHARACTERS || !Array.isArray(job.format) || job.format.length > MAX_TRANSFORM_NODES) {
|
|
206
|
+
throw new RangeError('The snippet transform batch is too large.');
|
|
207
|
+
}
|
|
208
|
+
if (
|
|
209
|
+
typeof job.flags !== 'string' ||
|
|
210
|
+
Array.from(job.flags as string).some((flag) => !'gimsu'.includes(flag)) ||
|
|
211
|
+
new Set(job.flags).size !== job.flags.length
|
|
212
|
+
) {
|
|
213
|
+
throw new SyntaxError('Snippet transforms support the flags g, i, m, s and u once each.');
|
|
214
|
+
}
|
|
215
|
+
let formatLength = 0;
|
|
216
|
+
for (const node of job.format) {
|
|
217
|
+
if (node.kind === 'text') {
|
|
218
|
+
if (typeof node.text !== 'string') {
|
|
219
|
+
throw new SyntaxError('Invalid transform text.');
|
|
220
|
+
}
|
|
221
|
+
formatLength += node.text.length;
|
|
222
|
+
} else if (node.kind === 'capture' || node.kind === 'conditional') {
|
|
223
|
+
if (!Number.isInteger(node.index) || node.index < 0 || node.index > 99) {
|
|
224
|
+
throw new RangeError('Invalid transform capture index.');
|
|
225
|
+
}
|
|
226
|
+
if (node.kind === 'capture' && node.case !== undefined && !CASES.has(node.case)) {
|
|
227
|
+
throw new SyntaxError('Invalid transform case operation.');
|
|
228
|
+
}
|
|
229
|
+
if (node.kind === 'conditional') {
|
|
230
|
+
if ((node.if !== undefined && typeof node.if !== 'string') || (node.else !== undefined && typeof node.else !== 'string')) {
|
|
231
|
+
throw new SyntaxError('Invalid transform branches.');
|
|
232
|
+
}
|
|
233
|
+
formatLength += (node.if?.length ?? 0) + (node.else?.length ?? 0);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
throw new SyntaxError('Invalid transform format node.');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (formatLength > MAX_TRANSFORM_FORMAT) {
|
|
240
|
+
throw new RangeError('The transform format is too long.');
|
|
241
|
+
}
|
|
242
|
+
characters += formatLength;
|
|
243
|
+
if (characters > MAX_TRANSFORM_CHARACTERS) {
|
|
244
|
+
throw new RangeError('The snippet transform batch is too large.');
|
|
245
|
+
}
|
|
246
|
+
const key = job.flags + '\0' + job.pattern;
|
|
247
|
+
if (!patterns.has(key)) {
|
|
248
|
+
// Compilation validates syntax; only the worker executes patterns supplied by a server.
|
|
249
|
+
new RegExp(job.pattern, job.flags);
|
|
250
|
+
patterns.add(key);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
export interface SnippetTransformBudget {
|
|
255
|
+
remaining: number;
|
|
256
|
+
matches: number;
|
|
257
|
+
}
|
|
258
|
+
export function createSnippetTransformBudget(): SnippetTransformBudget {
|
|
259
|
+
return { remaining: MAX_TRANSFORM_WORK, matches: 0 };
|
|
260
|
+
}
|
|
261
|
+
function spend(budget: SnippetTransformBudget, amount: number): void {
|
|
262
|
+
budget.remaining -= amount;
|
|
263
|
+
if (budget.remaining < 0) {
|
|
264
|
+
throw new RangeError('The snippet transform work budget was exceeded.');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
export function applySnippetTransform(
|
|
268
|
+
job: SnippetTransformJob,
|
|
269
|
+
maximum = MAX_TRANSFORM_CHARACTERS,
|
|
270
|
+
expression = new RegExp(job.pattern, job.flags),
|
|
271
|
+
budget = createSnippetTransformBudget()
|
|
272
|
+
): string {
|
|
273
|
+
expression.lastIndex = 0;
|
|
274
|
+
spend(budget, job.value.length);
|
|
275
|
+
const pieces: string[] = [];
|
|
276
|
+
let at = 0,
|
|
277
|
+
length = 0,
|
|
278
|
+
matches = 0;
|
|
279
|
+
const append = (text: string): void => {
|
|
280
|
+
if (text === '') {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
length += text.length;
|
|
284
|
+
if (length > maximum) {
|
|
285
|
+
throw new RangeError('The snippet transform output is too large.');
|
|
286
|
+
}
|
|
287
|
+
pieces.push(text);
|
|
288
|
+
};
|
|
289
|
+
const format = (captures: readonly (string | undefined)[]): void => {
|
|
290
|
+
for (const node of job.format) {
|
|
291
|
+
spend(budget, 1);
|
|
292
|
+
if (node.kind === 'text') {
|
|
293
|
+
append(node.text);
|
|
294
|
+
} else {
|
|
295
|
+
const capture = captures[node.index] ?? '';
|
|
296
|
+
if (node.kind === 'capture' && node.case !== undefined) {
|
|
297
|
+
spend(budget, capture.length);
|
|
298
|
+
}
|
|
299
|
+
append(node.kind === 'capture' ? changeCase(capture, node.case) : capture !== '' ? (node.if ?? capture) : (node.else ?? ''));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
for (let match = expression.exec(job.value); match !== null; match = expression.exec(job.value)) {
|
|
304
|
+
if (++budget.matches > MAX_TRANSFORM_MATCHES) {
|
|
305
|
+
throw new RangeError('The snippet transform batch has too many matches.');
|
|
306
|
+
}
|
|
307
|
+
matches++;
|
|
308
|
+
spend(budget, 1);
|
|
309
|
+
append(job.value.slice(at, match.index));
|
|
310
|
+
format(match);
|
|
311
|
+
at = match.index + match[0].length;
|
|
312
|
+
if (!expression.global) {
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
if (match[0].length === 0) {
|
|
316
|
+
expression.lastIndex += expression.unicode && (job.value.codePointAt(expression.lastIndex) ?? 0) > 0xffff ? 2 : 1;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (matches === 0 && job.format.some((node) => node.kind === 'conditional' && Boolean(node.else))) {
|
|
320
|
+
format([]);
|
|
321
|
+
} else {
|
|
322
|
+
append(job.value.slice(at));
|
|
323
|
+
}
|
|
324
|
+
return pieces.join('');
|
|
325
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const MAX_TRANSFORM_JOBS = 1000;
|
|
2
|
+
export const MAX_TRANSFORM_VALUE = 100_000;
|
|
3
|
+
export const MAX_TRANSFORM_CHARACTERS = 2_000_000;
|
|
4
|
+
export const MAX_TRANSFORM_MATCHES = 10_000;
|
|
5
|
+
export const MAX_TRANSFORM_WORK = 1_000_000;
|
|
6
|
+
export const MAX_TRANSFORM_PATTERN = 1024;
|
|
7
|
+
export const MAX_TRANSFORM_FORMAT = 10_000;
|
|
8
|
+
export const MAX_TRANSFORM_NODES = 1000;
|
|
9
|
+
export type SnippetTransformCase = 'upcase' | 'downcase' | 'capitalize' | 'camelcase' | 'pascalcase';
|
|
10
|
+
export type SnippetTransformFormatNode =
|
|
11
|
+
| { readonly kind: 'text'; readonly text: string }
|
|
12
|
+
| { readonly kind: 'capture'; readonly index: number; readonly case?: SnippetTransformCase }
|
|
13
|
+
| { readonly kind: 'conditional'; readonly index: number; readonly if?: string; readonly else?: string };
|
|
14
|
+
export interface SnippetTransformJob {
|
|
15
|
+
readonly value: string;
|
|
16
|
+
readonly pattern: string;
|
|
17
|
+
readonly flags: string;
|
|
18
|
+
readonly format: readonly SnippetTransformFormatNode[];
|
|
19
|
+
}
|
|
20
|
+
export type SnippetTransformInput = { readonly type: 'transform'; readonly id: number; readonly jobs: readonly SnippetTransformJob[] };
|
|
21
|
+
export type SnippetTransformOutput =
|
|
22
|
+
| { readonly type: 'ready' }
|
|
23
|
+
| { readonly type: 'transformed'; readonly id: number; readonly values: readonly string[] }
|
|
24
|
+
| { readonly type: 'error'; readonly id: number; readonly kind: 'syntax' | 'range' | 'error'; readonly message: string };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { WorkerRequestPort } from '@adecore/editor/worker-client';
|
|
2
|
+
import { createSnippetTransformClient, SnippetTransformError, type SnippetTransformClient } from './snippet-transform-client.ts';
|
|
3
|
+
import { validateSnippetTransformJobs } from './snippet-transform-format.ts';
|
|
4
|
+
import { MAX_TRANSFORM_WORK, type SnippetTransformJob } from './snippet-transform-protocol.ts';
|
|
5
|
+
import type { ProjectLanguage } from './project-language.ts';
|
|
6
|
+
import { realTimers, type Timers } from './timers.ts';
|
|
7
|
+
|
|
8
|
+
interface RequestOptions {
|
|
9
|
+
readonly kind?: 'accept' | 'exit';
|
|
10
|
+
readonly current?: () => boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface SnippetTransformLease {
|
|
13
|
+
request(jobs: readonly SnippetTransformJob[], options?: RequestOptions): Promise<readonly string[]>;
|
|
14
|
+
dispose(): void;
|
|
15
|
+
}
|
|
16
|
+
interface Job {
|
|
17
|
+
readonly owner: symbol;
|
|
18
|
+
readonly kind: 'accept' | 'exit';
|
|
19
|
+
readonly jobs: readonly SnippetTransformJob[];
|
|
20
|
+
readonly current: () => boolean;
|
|
21
|
+
readonly resolve: (values: readonly string[]) => void;
|
|
22
|
+
readonly reject: (error: unknown) => void;
|
|
23
|
+
}
|
|
24
|
+
export class SnippetTransformQueueError extends Error {}
|
|
25
|
+
|
|
26
|
+
const IDLE_RELEASE_MS = 1000;
|
|
27
|
+
const MAX_PENDING = 16;
|
|
28
|
+
|
|
29
|
+
export class SnippetTransforms {
|
|
30
|
+
private readonly client: SnippetTransformClient;
|
|
31
|
+
private readonly timers: Timers;
|
|
32
|
+
private readonly owners = new Set<symbol>();
|
|
33
|
+
private pending: Job[] = [];
|
|
34
|
+
private active: Job | null = null;
|
|
35
|
+
private idle: unknown;
|
|
36
|
+
private disposed = false;
|
|
37
|
+
constructor(factory: () => WorkerRequestPort, timers: Timers = realTimers) {
|
|
38
|
+
this.timers = timers;
|
|
39
|
+
this.client = createSnippetTransformClient({
|
|
40
|
+
workerFactory: factory,
|
|
41
|
+
schedule: (callback, delay) => {
|
|
42
|
+
const timer = timers.set(callback, delay);
|
|
43
|
+
return () => timers.clear(timer);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
acquire(): SnippetTransformLease {
|
|
48
|
+
const owner = Symbol('snippet');
|
|
49
|
+
this.owners.add(owner);
|
|
50
|
+
return {
|
|
51
|
+
request: (jobs, options = {}) => this.request(owner, jobs, options),
|
|
52
|
+
dispose: () => this.releaseOwner(owner)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
private request(owner: symbol, jobs: readonly SnippetTransformJob[], options: RequestOptions): Promise<readonly string[]> {
|
|
56
|
+
if (this.disposed || !this.owners.has(owner)) {
|
|
57
|
+
return Promise.reject(new SnippetTransformError('disposed', 'The snippet owner was disposed.'));
|
|
58
|
+
}
|
|
59
|
+
let copied: SnippetTransformJob[];
|
|
60
|
+
try {
|
|
61
|
+
validateSnippetTransformJobs(jobs);
|
|
62
|
+
if (jobs.reduce((sum, job) => sum + job.value.length, 0) > MAX_TRANSFORM_WORK) {
|
|
63
|
+
throw new RangeError('The snippet transform inputs exceed the work budget.');
|
|
64
|
+
}
|
|
65
|
+
copied = jobs.map((job) => ({ ...job, format: job.format.map((node) => ({ ...node })) }));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return Promise.reject(
|
|
68
|
+
new SnippetTransformError(error instanceof RangeError ? 'range' : 'syntax', error instanceof Error ? error.message : String(error))
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (copied.length === 0) {
|
|
72
|
+
return Promise.resolve([]);
|
|
73
|
+
}
|
|
74
|
+
this.pending = this.pending.filter((job) => {
|
|
75
|
+
if (job.current()) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
job.reject(new SnippetTransformError('superseded', 'The queued snippet changed.'));
|
|
79
|
+
return false;
|
|
80
|
+
});
|
|
81
|
+
const kind = options.kind ?? 'accept';
|
|
82
|
+
if (kind === 'accept') {
|
|
83
|
+
this.pending = this.pending.filter((job) => {
|
|
84
|
+
if (job.owner !== owner || job.kind !== 'accept') {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
job.reject(new SnippetTransformError('superseded', 'A newer snippet accept replaced the queued one.'));
|
|
88
|
+
return false;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (this.pending.length >= MAX_PENDING) {
|
|
92
|
+
return Promise.reject(new SnippetTransformQueueError('Too many queued snippet transforms.'));
|
|
93
|
+
}
|
|
94
|
+
this.timers.clear(this.idle);
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
this.pending.push({ owner, kind, jobs: copied, current: () => this.owners.has(owner) && (options.current?.() ?? true), resolve, reject });
|
|
97
|
+
queueMicrotask(() => this.pump());
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
private pump(): void {
|
|
101
|
+
if (this.disposed || this.active !== null) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const job = this.pending.shift();
|
|
105
|
+
if (job === undefined) {
|
|
106
|
+
this.timers.clear(this.idle);
|
|
107
|
+
this.idle = this.timers.set(() => this.client.release(), IDLE_RELEASE_MS);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!job.current()) {
|
|
111
|
+
job.reject(new SnippetTransformError('superseded', 'The queued snippet changed.'));
|
|
112
|
+
this.pump();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.active = job;
|
|
116
|
+
void this.client
|
|
117
|
+
.transform(job.jobs)
|
|
118
|
+
.then((values) => {
|
|
119
|
+
if (!job.current()) {
|
|
120
|
+
throw new SnippetTransformError('superseded', 'The snippet changed while transforming.');
|
|
121
|
+
}
|
|
122
|
+
job.resolve(values);
|
|
123
|
+
})
|
|
124
|
+
.catch((error) => job.reject(error))
|
|
125
|
+
.finally(() => {
|
|
126
|
+
this.active = null;
|
|
127
|
+
this.pump();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
private releaseOwner(owner: symbol): void {
|
|
131
|
+
if (!this.owners.delete(owner)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.pending = this.pending.filter((job) => {
|
|
135
|
+
if (job.owner !== owner) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
job.reject(new SnippetTransformError('disposed', 'The snippet owner was disposed.'));
|
|
139
|
+
return false;
|
|
140
|
+
});
|
|
141
|
+
if (this.active?.owner === owner) {
|
|
142
|
+
this.active.reject(new SnippetTransformError('disposed', 'The snippet owner was disposed.'));
|
|
143
|
+
this.client.cancel();
|
|
144
|
+
}
|
|
145
|
+
if (this.owners.size === 0) {
|
|
146
|
+
this.dispose();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
dispose(): void {
|
|
150
|
+
if (this.disposed) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.disposed = true;
|
|
154
|
+
this.timers.clear(this.idle);
|
|
155
|
+
this.owners.clear();
|
|
156
|
+
const error = new SnippetTransformError('disposed', 'The project snippet transforms were disposed.');
|
|
157
|
+
this.active?.reject(error);
|
|
158
|
+
this.pending.forEach((job) => job.reject(error));
|
|
159
|
+
this.pending = [];
|
|
160
|
+
this.client.dispose();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
interface Entry {
|
|
164
|
+
readonly queue: SnippetTransforms;
|
|
165
|
+
references: number;
|
|
166
|
+
}
|
|
167
|
+
const projects = new WeakMap<ProjectLanguage, Entry>();
|
|
168
|
+
export function acquireSnippetTransforms(project: ProjectLanguage, timers: Timers): SnippetTransformLease | null {
|
|
169
|
+
const factory = project.host.snippetTransformWorker;
|
|
170
|
+
if (factory === undefined) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
let entry = projects.get(project);
|
|
174
|
+
if (entry === undefined) {
|
|
175
|
+
entry = { queue: new SnippetTransforms(factory, timers), references: 0 };
|
|
176
|
+
projects.set(project, entry);
|
|
177
|
+
}
|
|
178
|
+
const held = entry,
|
|
179
|
+
lease = held.queue.acquire();
|
|
180
|
+
held.references++;
|
|
181
|
+
let disposed = false;
|
|
182
|
+
return {
|
|
183
|
+
request: (jobs, options) => lease.request(jobs, options),
|
|
184
|
+
dispose: () => {
|
|
185
|
+
if (disposed) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
disposed = true;
|
|
189
|
+
lease.dispose();
|
|
190
|
+
if (--held.references === 0 && projects.get(project) === held) {
|
|
191
|
+
projects.delete(project);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
export function disposeSnippetTransforms(project: ProjectLanguage): void {
|
|
197
|
+
projects.get(project)?.queue.dispose();
|
|
198
|
+
projects.delete(project);
|
|
199
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { applySnippetTransform, createSnippetTransformBudget, validateSnippetTransformJobs } from './snippet-transform-format.ts';
|
|
2
|
+
import { MAX_TRANSFORM_CHARACTERS, type SnippetTransformInput, type SnippetTransformOutput } from './snippet-transform-protocol.ts';
|
|
3
|
+
export {
|
|
4
|
+
createSnippetTransformClient,
|
|
5
|
+
SnippetTransformError,
|
|
6
|
+
type SnippetTransformClient,
|
|
7
|
+
type SnippetTransformClientOptions,
|
|
8
|
+
type SnippetTransformErrorKind
|
|
9
|
+
} from './snippet-transform-client.ts';
|
|
10
|
+
export { compileSnippetTransformFormat } from './snippet-transform-format.ts';
|
|
11
|
+
export type {
|
|
12
|
+
SnippetTransformJob,
|
|
13
|
+
SnippetTransformInput,
|
|
14
|
+
SnippetTransformOutput,
|
|
15
|
+
SnippetTransformCase,
|
|
16
|
+
SnippetTransformFormatNode
|
|
17
|
+
} from './snippet-transform-protocol.ts';
|
|
18
|
+
export interface SnippetTransformWorkerEndpoint {
|
|
19
|
+
addEventListener(type: 'message', listener: (event: MessageEvent<SnippetTransformInput>) => void): void;
|
|
20
|
+
removeEventListener(type: 'message', listener: (event: MessageEvent<SnippetTransformInput>) => void): void;
|
|
21
|
+
postMessage(message: SnippetTransformOutput): void;
|
|
22
|
+
}
|
|
23
|
+
export function serveSnippetTransforms(endpoint: SnippetTransformWorkerEndpoint): () => void {
|
|
24
|
+
const receive = ({ data: request }: MessageEvent<SnippetTransformInput>): void => {
|
|
25
|
+
const id = request !== null && typeof request === 'object' && Number.isSafeInteger(request.id) && request.id > 0 ? request.id : 0;
|
|
26
|
+
try {
|
|
27
|
+
if (id === 0 || request.type !== 'transform') {
|
|
28
|
+
throw new RangeError('Invalid snippet transform request.');
|
|
29
|
+
}
|
|
30
|
+
validateSnippetTransformJobs(request.jobs);
|
|
31
|
+
let remaining = MAX_TRANSFORM_CHARACTERS;
|
|
32
|
+
const budget = createSnippetTransformBudget();
|
|
33
|
+
const patterns = new Map<string, RegExp>();
|
|
34
|
+
const values = request.jobs.map((job) => {
|
|
35
|
+
const key = job.flags + '\0' + job.pattern;
|
|
36
|
+
let expression = patterns.get(key);
|
|
37
|
+
if (expression === undefined) {
|
|
38
|
+
expression = new RegExp(job.pattern, job.flags);
|
|
39
|
+
patterns.set(key, expression);
|
|
40
|
+
}
|
|
41
|
+
const value = applySnippetTransform(job, remaining, expression, budget);
|
|
42
|
+
remaining -= value.length;
|
|
43
|
+
return value;
|
|
44
|
+
});
|
|
45
|
+
endpoint.postMessage({ type: 'transformed', id: request.id, values });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
endpoint.postMessage({
|
|
48
|
+
type: 'error',
|
|
49
|
+
id,
|
|
50
|
+
kind: error instanceof SyntaxError ? 'syntax' : error instanceof RangeError ? 'range' : 'error',
|
|
51
|
+
message: error instanceof Error ? error.message : String(error)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
endpoint.addEventListener('message', receive);
|
|
56
|
+
endpoint.postMessage({ type: 'ready' });
|
|
57
|
+
return () => endpoint.removeEventListener('message', receive);
|
|
58
|
+
}
|