@adecore/editor-react 0.39.0 → 0.40.1
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.js +30 -55
- package/dist/CodeAuthorsCard.js +0 -1
- package/dist/CompletionPopup.js +150 -181
- package/dist/DiffView.d.ts +1 -1
- package/dist/DiffView.js +2 -29
- package/dist/PickPopup.js +54 -80
- package/dist/SplitDiffView.js +27 -57
- package/dist/SymbolPicker.js +2 -8
- package/dist/code-actions-model.js +2 -9
- package/dist/code-actions.js +10 -11
- package/dist/code-vision.js +2 -1
- package/dist/completion-model.js +2 -2
- package/dist/completion.js +13 -34
- package/dist/definition-link.d.ts +1 -0
- package/dist/definition-link.js +22 -18
- package/dist/diagnostics-model.d.ts +1 -0
- package/dist/diagnostics-model.js +3 -1
- package/dist/diagnostics.js +1 -2
- package/dist/diff-syntax.d.ts +1 -0
- package/dist/diff-syntax.js +31 -0
- package/dist/doc-comment-model.d.ts +1 -0
- package/dist/doc-comment-model.js +1 -1
- package/dist/folding.js +2 -3
- package/dist/format-on-type.js +6 -10
- package/dist/hover-content.d.ts +0 -2
- package/dist/hover-content.js +1 -9
- package/dist/inlay-hints.d.ts +0 -1
- package/dist/inlay-hints.js +11 -41
- package/dist/navigation.js +4 -14
- package/dist/peek.js +2 -12
- package/dist/popup-focus.d.ts +3 -1
- package/dist/popup-focus.js +33 -0
- package/dist/recent-choices.d.ts +0 -1
- package/dist/recent-choices.js +0 -4
- package/dist/rendered-docs.d.ts +1 -0
- package/dist/rendered-docs.js +15 -28
- package/dist/snippet-field-transforms.d.ts +5 -0
- package/dist/snippet-field-transforms.js +27 -16
- package/dist/snippet-session.js +8 -20
- package/dist/symbol-links.js +3 -10
- package/dist/symbol-picker-model.d.ts +1 -0
- package/dist/symbol-picker-model.js +7 -0
- package/dist/unique.d.ts +10 -0
- package/dist/unique.js +15 -0
- package/package.json +9 -9
- package/src/AnchoredPopup.tsx +2 -0
- package/src/BaselineStatus.tsx +1 -0
- package/src/CallHierarchyPanel.tsx +16 -6
- package/src/ChangeReview.tsx +7 -0
- package/src/CodeAuthorsCard.tsx +2 -5
- package/src/CompletionPopup.tsx +21 -15
- package/src/DiffView.tsx +53 -30
- package/src/EditorContextMenu.tsx +7 -0
- package/src/EditorView.tsx +28 -0
- package/src/FindReplace.tsx +8 -0
- package/src/HoverCard.tsx +8 -0
- package/src/HoverSections.tsx +11 -0
- package/src/LanguagePopups.tsx +1 -0
- package/src/PanelResizeHandle.tsx +8 -0
- package/src/PathText.tsx +1 -0
- package/src/PeekPanel.tsx +13 -0
- package/src/PickPopup.tsx +7 -6
- package/src/RenameCard.tsx +13 -0
- package/src/RenderedDocs.tsx +11 -0
- package/src/SplitDiffView.tsx +91 -56
- package/src/SymbolPicker.tsx +10 -18
- package/src/call-hierarchy-model.ts +10 -0
- package/src/call-hierarchy.ts +82 -0
- package/src/code-actions-model.ts +21 -9
- package/src/code-actions.ts +83 -7
- package/src/code-authors.ts +20 -0
- package/src/code-vision-model.ts +37 -0
- package/src/code-vision.ts +47 -3
- package/src/completion-call.ts +19 -0
- package/src/completion-model.ts +58 -2
- package/src/completion.ts +156 -30
- package/src/conflict-model.ts +4 -0
- package/src/context-menu-rows.ts +1 -0
- package/src/context-menu.ts +7 -0
- package/src/definition-link.ts +42 -18
- package/src/diagnostics-model.ts +19 -1
- package/src/diagnostics.ts +31 -2
- package/src/diff-expansion.ts +9 -0
- package/src/diff-overlays.tsx +8 -0
- package/src/diff-syntax.ts +54 -0
- package/src/diff-wordmarks.ts +8 -0
- package/src/doc-comment-model.ts +51 -1
- package/src/editor-language.ts +11 -0
- package/src/find-query.ts +7 -0
- package/src/folding.ts +10 -3
- package/src/format-on-type.ts +23 -10
- package/src/highlight-layers.ts +3 -0
- package/src/highlights.ts +18 -0
- package/src/history.ts +11 -0
- package/src/holder.ts +3 -0
- package/src/hover-content.ts +34 -17
- package/src/hover.ts +57 -0
- package/src/index.ts +4 -0
- package/src/inlay-hints.ts +86 -47
- package/src/key-priority.ts +2 -0
- package/src/line-edits.ts +8 -0
- package/src/models.ts +6 -0
- package/src/navigation-history.ts +5 -0
- package/src/navigation.ts +28 -13
- package/src/panel-size.ts +7 -0
- package/src/patch-decorations.ts +39 -0
- package/src/patch-layout.ts +47 -0
- package/src/peek-model.ts +21 -0
- package/src/peek.ts +49 -13
- package/src/pick.ts +35 -0
- package/src/popup-focus.ts +11 -1
- package/src/popup-placement.ts +5 -0
- package/src/popups.ts +2 -6
- package/src/project-language.ts +33 -0
- package/src/project-problems.ts +11 -0
- package/src/proposal-model.ts +40 -0
- package/src/recent-choices.ts +3 -5
- package/src/refresher.ts +3 -0
- package/src/rename-model.ts +28 -0
- package/src/rename.ts +56 -0
- package/src/rendered-docs.ts +89 -19
- package/src/review-group.ts +4 -0
- package/src/row-host.ts +12 -0
- package/src/selection-ranges.ts +6 -0
- package/src/semantic-delta.ts +7 -0
- package/src/semantic-model.ts +11 -0
- package/src/semantic-tokens.ts +37 -0
- package/src/shortcut-keys.ts +3 -0
- package/src/signature-model.ts +7 -0
- package/src/signature.ts +27 -0
- package/src/snippet-field-transforms.ts +48 -13
- package/src/snippet-indent.ts +10 -0
- package/src/snippet-range-order.ts +5 -0
- package/src/snippet-session.ts +173 -31
- package/src/snippet-transform-client.ts +14 -0
- package/src/snippet-transform-format.ts +98 -0
- package/src/snippet-transform-protocol.ts +5 -0
- package/src/snippet-transform-queue.ts +53 -0
- package/src/snippet-transform-worker.ts +14 -0
- package/src/snippet-variables.ts +9 -0
- package/src/snippet.ts +107 -0
- package/src/split-alignment.ts +10 -0
- package/src/split-scroll.ts +8 -0
- package/src/symbol-links.ts +9 -12
- package/src/symbol-picker-model.ts +25 -0
- package/src/symbol-picker.ts +9 -0
- package/src/symbols.ts +21 -0
- package/src/testing.ts +7 -0
- package/src/timers.ts +5 -0
- package/src/unique.ts +23 -0
- package/src/use-problem-counts.ts +4 -0
- package/src/view-state.ts +9 -0
- package/src/word-diff.ts +5 -0
- package/src/workspace-edit.ts +84 -0
|
@@ -1,33 +1,41 @@
|
|
|
1
1
|
import { createWorkerRequestClient, WorkerRequestError, type WorkerRequestPort } from '@adecore/editor/worker-client';
|
|
2
2
|
import { validateSnippetTransformJobs } from './snippet-transform-format.ts';
|
|
3
3
|
import { MAX_TRANSFORM_CHARACTERS, type SnippetTransformInput, type SnippetTransformOutput, type SnippetTransformJob } from './snippet-transform-protocol.ts';
|
|
4
|
+
|
|
4
5
|
export type SnippetTransformErrorKind = 'syntax' | 'range' | 'error' | 'timeout' | 'superseded' | 'unavailable' | 'disposed';
|
|
6
|
+
|
|
5
7
|
export class SnippetTransformError extends Error {
|
|
6
8
|
readonly kind: SnippetTransformErrorKind;
|
|
9
|
+
|
|
7
10
|
constructor(kind: SnippetTransformErrorKind, message: string) {
|
|
8
11
|
super(message);
|
|
9
12
|
this.kind = kind;
|
|
10
13
|
this.name = 'SnippetTransformError';
|
|
11
14
|
}
|
|
12
15
|
}
|
|
16
|
+
|
|
13
17
|
export interface SnippetTransformClientOptions {
|
|
14
18
|
readonly workerFactory: () => WorkerRequestPort;
|
|
15
19
|
readonly deadline?: number;
|
|
16
20
|
readonly schedule?: (callback: () => void, delay: number) => () => void;
|
|
17
21
|
}
|
|
22
|
+
|
|
18
23
|
export interface SnippetTransformClient {
|
|
19
24
|
transform(jobs: readonly SnippetTransformJob[]): Promise<readonly string[]>;
|
|
20
25
|
cancel(): void;
|
|
21
26
|
release(): void;
|
|
22
27
|
dispose(): void;
|
|
23
28
|
}
|
|
29
|
+
|
|
24
30
|
export function createSnippetTransformClient(options: SnippetTransformClientOptions): SnippetTransformClient {
|
|
25
31
|
const transport = createWorkerRequestClient<SnippetTransformInput, Exclude<SnippetTransformOutput, { type: 'ready' }>>({
|
|
26
32
|
...options,
|
|
27
33
|
failure: (reply) =>
|
|
28
34
|
reply.type === 'error' ? { error: new SnippetTransformError(reply.kind, reply.message), restart: reply.kind === 'error' } : undefined
|
|
29
35
|
});
|
|
36
|
+
|
|
30
37
|
let disposed = false;
|
|
38
|
+
|
|
31
39
|
const failure = (error: unknown): SnippetTransformError =>
|
|
32
40
|
error instanceof SnippetTransformError
|
|
33
41
|
? error
|
|
@@ -37,21 +45,26 @@ export function createSnippetTransformClient(options: SnippetTransformClientOpti
|
|
|
37
45
|
error instanceof SyntaxError ? 'syntax' : error instanceof RangeError ? 'range' : 'error',
|
|
38
46
|
error instanceof Error ? error.message : String(error)
|
|
39
47
|
);
|
|
48
|
+
|
|
40
49
|
return {
|
|
41
50
|
transform(jobs) {
|
|
42
51
|
if (disposed) {
|
|
43
52
|
return Promise.reject(new SnippetTransformError('disposed', 'The snippet transform client was disposed.'));
|
|
44
53
|
}
|
|
54
|
+
|
|
45
55
|
if (Array.isArray(jobs) && jobs.length === 0) {
|
|
46
56
|
return Promise.resolve([]);
|
|
47
57
|
}
|
|
58
|
+
|
|
48
59
|
let copied: SnippetTransformJob[];
|
|
60
|
+
|
|
49
61
|
try {
|
|
50
62
|
validateSnippetTransformJobs(jobs);
|
|
51
63
|
copied = jobs.map((job) => ({ ...job, format: job.format.map((node) => ({ ...node })) }));
|
|
52
64
|
} catch (error) {
|
|
53
65
|
return Promise.reject(failure(error));
|
|
54
66
|
}
|
|
67
|
+
|
|
55
68
|
return transport
|
|
56
69
|
.request((id) => ({ type: 'transform', id, jobs: copied }))
|
|
57
70
|
.then((reply) => {
|
|
@@ -64,6 +77,7 @@ export function createSnippetTransformClient(options: SnippetTransformClientOpti
|
|
|
64
77
|
) {
|
|
65
78
|
throw new SnippetTransformError('error', 'Invalid snippet transform response.');
|
|
66
79
|
}
|
|
80
|
+
|
|
67
81
|
return reply.values;
|
|
68
82
|
})
|
|
69
83
|
.catch((error) => {
|
|
@@ -11,85 +11,116 @@ import {
|
|
|
11
11
|
type SnippetTransformFormatNode,
|
|
12
12
|
type SnippetTransformJob
|
|
13
13
|
} from './snippet-transform-protocol.ts';
|
|
14
|
+
|
|
14
15
|
const CASES: ReadonlySet<string> = new Set(['upcase', 'downcase', 'capitalize', 'camelcase', 'pascalcase']);
|
|
16
|
+
|
|
15
17
|
function readFormat(source: string, start: number, stopAtSlash: boolean): { nodes: readonly SnippetTransformFormatNode[]; end: number } {
|
|
16
18
|
const nodes: SnippetTransformFormatNode[] = [];
|
|
19
|
+
|
|
17
20
|
let at = start,
|
|
18
21
|
text = '';
|
|
22
|
+
|
|
19
23
|
const bound = (): void => {
|
|
20
24
|
if (at - start > MAX_TRANSFORM_FORMAT) {
|
|
21
25
|
throw new RangeError('The transform format is too long.');
|
|
22
26
|
}
|
|
23
27
|
};
|
|
28
|
+
|
|
24
29
|
const push = (node: SnippetTransformFormatNode): void => {
|
|
25
30
|
if (nodes.length >= MAX_TRANSFORM_NODES) {
|
|
26
31
|
throw new RangeError('The transform format has too many nodes.');
|
|
27
32
|
}
|
|
33
|
+
|
|
28
34
|
nodes.push(node);
|
|
29
35
|
};
|
|
36
|
+
|
|
30
37
|
const flush = (): void => {
|
|
31
38
|
if (text !== '') {
|
|
32
39
|
push({ kind: 'text', text });
|
|
33
40
|
text = '';
|
|
34
41
|
}
|
|
35
42
|
};
|
|
43
|
+
|
|
36
44
|
const string = (colon = false): string => {
|
|
37
45
|
let value = '';
|
|
46
|
+
|
|
38
47
|
while (at < source.length && source[at] !== '}' && !(colon && source[at] === ':')) {
|
|
39
48
|
if (source[at] === '\\') {
|
|
40
49
|
if (!['$', '}', '\\'].includes(source[at + 1] ?? '')) {
|
|
41
50
|
throw new SyntaxError('Invalid conditional transform escape.');
|
|
42
51
|
}
|
|
52
|
+
|
|
43
53
|
at++;
|
|
44
54
|
}
|
|
55
|
+
|
|
45
56
|
value += source[at++]!;
|
|
46
57
|
bound();
|
|
47
58
|
}
|
|
59
|
+
|
|
48
60
|
return value;
|
|
49
61
|
};
|
|
62
|
+
|
|
50
63
|
while (at < source.length && !(stopAtSlash && source[at] === '/')) {
|
|
51
64
|
bound();
|
|
65
|
+
|
|
52
66
|
if (source[at] === '\\' && (source[at + 1] === '\\' || source[at + 1] === '/')) {
|
|
53
67
|
at++;
|
|
54
68
|
text += source[at++]!;
|
|
69
|
+
|
|
55
70
|
continue;
|
|
56
71
|
}
|
|
72
|
+
|
|
57
73
|
if (source[at] !== '$') {
|
|
58
74
|
text += source[at++]!;
|
|
75
|
+
|
|
59
76
|
continue;
|
|
60
77
|
}
|
|
78
|
+
|
|
61
79
|
at++;
|
|
62
80
|
const brace = source[at] === '{';
|
|
81
|
+
|
|
63
82
|
if (brace) {
|
|
64
83
|
at++;
|
|
65
84
|
}
|
|
85
|
+
|
|
66
86
|
const start = at;
|
|
87
|
+
|
|
67
88
|
while (at < source.length && source.charCodeAt(at) >= 48 && source.charCodeAt(at) <= 57) {
|
|
68
89
|
at++;
|
|
69
90
|
}
|
|
91
|
+
|
|
70
92
|
if (at === start) {
|
|
71
93
|
if (brace) {
|
|
72
94
|
throw new SyntaxError('A transform format needs a capture index.');
|
|
73
95
|
}
|
|
96
|
+
|
|
74
97
|
text += '$';
|
|
98
|
+
|
|
75
99
|
continue;
|
|
76
100
|
}
|
|
101
|
+
|
|
77
102
|
const index = Number(source.slice(start, at));
|
|
103
|
+
|
|
78
104
|
if (index > 99) {
|
|
79
105
|
throw new RangeError('Transform captures are limited to 99.');
|
|
80
106
|
}
|
|
107
|
+
|
|
81
108
|
flush();
|
|
82
109
|
let node: SnippetTransformFormatNode = { kind: 'capture', index };
|
|
110
|
+
|
|
83
111
|
if (brace) {
|
|
84
112
|
if (source[at] === ':') {
|
|
85
113
|
at++;
|
|
86
114
|
const operator = source[at];
|
|
115
|
+
|
|
87
116
|
if (operator === '/') {
|
|
88
117
|
at++;
|
|
89
118
|
const operation = string();
|
|
119
|
+
|
|
90
120
|
if (!CASES.has(operation)) {
|
|
91
121
|
throw new SyntaxError('Unknown transform case operation.');
|
|
92
122
|
}
|
|
123
|
+
|
|
93
124
|
node = { kind: 'capture', index, case: operation as SnippetTransformCase };
|
|
94
125
|
} else if (operator === '+') {
|
|
95
126
|
at++;
|
|
@@ -97,33 +128,43 @@ function readFormat(source: string, start: number, stopAtSlash: boolean): { node
|
|
|
97
128
|
} else if (operator === '?') {
|
|
98
129
|
at++;
|
|
99
130
|
const when = string(true);
|
|
131
|
+
|
|
100
132
|
if (source[at++] !== ':') {
|
|
101
133
|
throw new SyntaxError('A conditional transform needs both branches.');
|
|
102
134
|
}
|
|
135
|
+
|
|
103
136
|
node = { kind: 'conditional', index, if: when, else: string() };
|
|
104
137
|
} else {
|
|
105
138
|
if (operator === '-') {
|
|
106
139
|
at++;
|
|
107
140
|
}
|
|
141
|
+
|
|
108
142
|
node = { kind: 'conditional', index, else: string() };
|
|
109
143
|
}
|
|
110
144
|
}
|
|
145
|
+
|
|
111
146
|
if (source[at++] !== '}') {
|
|
112
147
|
throw new SyntaxError('An unclosed transform capture cannot be applied.');
|
|
113
148
|
}
|
|
114
149
|
}
|
|
150
|
+
|
|
115
151
|
push(node);
|
|
116
152
|
}
|
|
153
|
+
|
|
117
154
|
flush();
|
|
118
155
|
bound();
|
|
156
|
+
|
|
119
157
|
if (stopAtSlash && source[at] !== '/') {
|
|
120
158
|
throw new SyntaxError('Unclosed snippet transform format.');
|
|
121
159
|
}
|
|
160
|
+
|
|
122
161
|
return { nodes, end: at };
|
|
123
162
|
}
|
|
163
|
+
|
|
124
164
|
export function compileSnippetTransformFormat(source: string): readonly SnippetTransformFormatNode[] {
|
|
125
165
|
return readFormat(source, 0, false).nodes;
|
|
126
166
|
}
|
|
167
|
+
|
|
127
168
|
export function scanSnippetTransform(
|
|
128
169
|
source: string,
|
|
129
170
|
at: number
|
|
@@ -131,67 +172,90 @@ export function scanSnippetTransform(
|
|
|
131
172
|
if (source[at++] !== '/') {
|
|
132
173
|
throw new SyntaxError('A snippet transform needs an opening slash.');
|
|
133
174
|
}
|
|
175
|
+
|
|
134
176
|
let pattern = '';
|
|
177
|
+
|
|
135
178
|
while (at < source.length && source[at] !== '/') {
|
|
136
179
|
const character = source[at++]!;
|
|
180
|
+
|
|
137
181
|
if (character === '\\' && source[at] === '/') {
|
|
138
182
|
pattern += source[at++]!;
|
|
139
183
|
} else {
|
|
140
184
|
pattern += character;
|
|
141
185
|
}
|
|
186
|
+
|
|
142
187
|
if (pattern.length > MAX_TRANSFORM_PATTERN) {
|
|
143
188
|
throw new RangeError('The snippet transform pattern is too long.');
|
|
144
189
|
}
|
|
145
190
|
}
|
|
191
|
+
|
|
146
192
|
if (source[at++] !== '/') {
|
|
147
193
|
throw new SyntaxError('Unclosed snippet transform pattern.');
|
|
148
194
|
}
|
|
195
|
+
|
|
149
196
|
const parsed = readFormat(source, at, true);
|
|
150
197
|
at = parsed.end + 1;
|
|
151
198
|
let flags = '';
|
|
199
|
+
|
|
152
200
|
while (at < source.length && source[at] !== '}') {
|
|
153
201
|
const flag = source[at++]!;
|
|
202
|
+
|
|
154
203
|
if (!'gimsu'.includes(flag) || flags.includes(flag)) {
|
|
155
204
|
throw new SyntaxError('Invalid snippet transform flags.');
|
|
156
205
|
}
|
|
206
|
+
|
|
157
207
|
flags += flag;
|
|
158
208
|
}
|
|
209
|
+
|
|
159
210
|
if (source[at++] !== '}') {
|
|
160
211
|
throw new SyntaxError('Unclosed snippet transform flags.');
|
|
161
212
|
}
|
|
213
|
+
|
|
162
214
|
return { pattern, flags, format: parsed.nodes, end: at };
|
|
163
215
|
}
|
|
216
|
+
|
|
164
217
|
function capitalize(value: string): string {
|
|
165
218
|
const first = value.slice(0, (value.codePointAt(0) ?? 0) > 0xffff ? 2 : 1);
|
|
219
|
+
|
|
166
220
|
return first.toUpperCase() + value.slice(first.length);
|
|
167
221
|
}
|
|
222
|
+
|
|
168
223
|
function changeCase(value: string, operation?: SnippetTransformCase): string {
|
|
169
224
|
if (operation === 'upcase') {
|
|
170
225
|
return value.toUpperCase();
|
|
171
226
|
}
|
|
227
|
+
|
|
172
228
|
if (operation === 'downcase') {
|
|
173
229
|
return value.toLowerCase();
|
|
174
230
|
}
|
|
231
|
+
|
|
175
232
|
if (operation === 'capitalize') {
|
|
176
233
|
return capitalize(value);
|
|
177
234
|
}
|
|
235
|
+
|
|
178
236
|
if (operation === 'camelcase' || operation === 'pascalcase') {
|
|
179
237
|
const words = value.match(/[\p{L}0-9]+/gu);
|
|
238
|
+
|
|
180
239
|
if (words === null) {
|
|
181
240
|
return value;
|
|
182
241
|
}
|
|
242
|
+
|
|
183
243
|
return words
|
|
184
244
|
.map((word, index) => (operation === 'camelcase' && index === 0 ? word.slice(0, 1).toLowerCase() + word.slice(1) : capitalize(word)))
|
|
185
245
|
.join('');
|
|
186
246
|
}
|
|
247
|
+
|
|
187
248
|
return value;
|
|
188
249
|
}
|
|
250
|
+
|
|
189
251
|
export function validateSnippetTransformJobs(jobs: readonly SnippetTransformJob[]): void {
|
|
190
252
|
if (!Array.isArray(jobs) || jobs.length > MAX_TRANSFORM_JOBS) {
|
|
191
253
|
throw new RangeError('Too many snippet transform jobs.');
|
|
192
254
|
}
|
|
255
|
+
|
|
193
256
|
let characters = 0;
|
|
194
257
|
const patterns = new Set<string>();
|
|
258
|
+
|
|
195
259
|
for (const job of jobs) {
|
|
196
260
|
if (
|
|
197
261
|
typeof job.value !== 'string' ||
|
|
@@ -201,10 +265,13 @@ export function validateSnippetTransformJobs(jobs: readonly SnippetTransformJob[
|
|
|
201
265
|
) {
|
|
202
266
|
throw new RangeError('A snippet transform value or pattern is too large.');
|
|
203
267
|
}
|
|
268
|
+
|
|
204
269
|
characters += job.value.length + job.pattern.length;
|
|
270
|
+
|
|
205
271
|
if (characters > MAX_TRANSFORM_CHARACTERS || !Array.isArray(job.format) || job.format.length > MAX_TRANSFORM_NODES) {
|
|
206
272
|
throw new RangeError('The snippet transform batch is too large.');
|
|
207
273
|
}
|
|
274
|
+
|
|
208
275
|
if (
|
|
209
276
|
typeof job.flags !== 'string' ||
|
|
210
277
|
Array.from(job.flags as string).some((flag) => !'gimsu'.includes(flag)) ||
|
|
@@ -212,38 +279,49 @@ export function validateSnippetTransformJobs(jobs: readonly SnippetTransformJob[
|
|
|
212
279
|
) {
|
|
213
280
|
throw new SyntaxError('Snippet transforms support the flags g, i, m, s and u once each.');
|
|
214
281
|
}
|
|
282
|
+
|
|
215
283
|
let formatLength = 0;
|
|
284
|
+
|
|
216
285
|
for (const node of job.format) {
|
|
217
286
|
if (node.kind === 'text') {
|
|
218
287
|
if (typeof node.text !== 'string') {
|
|
219
288
|
throw new SyntaxError('Invalid transform text.');
|
|
220
289
|
}
|
|
290
|
+
|
|
221
291
|
formatLength += node.text.length;
|
|
222
292
|
} else if (node.kind === 'capture' || node.kind === 'conditional') {
|
|
223
293
|
if (!Number.isInteger(node.index) || node.index < 0 || node.index > 99) {
|
|
224
294
|
throw new RangeError('Invalid transform capture index.');
|
|
225
295
|
}
|
|
296
|
+
|
|
226
297
|
if (node.kind === 'capture' && node.case !== undefined && !CASES.has(node.case)) {
|
|
227
298
|
throw new SyntaxError('Invalid transform case operation.');
|
|
228
299
|
}
|
|
300
|
+
|
|
229
301
|
if (node.kind === 'conditional') {
|
|
230
302
|
if ((node.if !== undefined && typeof node.if !== 'string') || (node.else !== undefined && typeof node.else !== 'string')) {
|
|
231
303
|
throw new SyntaxError('Invalid transform branches.');
|
|
232
304
|
}
|
|
305
|
+
|
|
233
306
|
formatLength += (node.if?.length ?? 0) + (node.else?.length ?? 0);
|
|
234
307
|
}
|
|
235
308
|
} else {
|
|
236
309
|
throw new SyntaxError('Invalid transform format node.');
|
|
237
310
|
}
|
|
238
311
|
}
|
|
312
|
+
|
|
239
313
|
if (formatLength > MAX_TRANSFORM_FORMAT) {
|
|
240
314
|
throw new RangeError('The transform format is too long.');
|
|
241
315
|
}
|
|
316
|
+
|
|
242
317
|
characters += formatLength;
|
|
318
|
+
|
|
243
319
|
if (characters > MAX_TRANSFORM_CHARACTERS) {
|
|
244
320
|
throw new RangeError('The snippet transform batch is too large.');
|
|
245
321
|
}
|
|
322
|
+
|
|
246
323
|
const key = job.flags + '\0' + job.pattern;
|
|
324
|
+
|
|
247
325
|
if (!patterns.has(key)) {
|
|
248
326
|
// Compilation validates syntax; only the worker executes patterns supplied by a server.
|
|
249
327
|
new RegExp(job.pattern, job.flags);
|
|
@@ -251,19 +329,24 @@ export function validateSnippetTransformJobs(jobs: readonly SnippetTransformJob[
|
|
|
251
329
|
}
|
|
252
330
|
}
|
|
253
331
|
}
|
|
332
|
+
|
|
254
333
|
export interface SnippetTransformBudget {
|
|
255
334
|
remaining: number;
|
|
256
335
|
matches: number;
|
|
257
336
|
}
|
|
337
|
+
|
|
258
338
|
export function createSnippetTransformBudget(): SnippetTransformBudget {
|
|
259
339
|
return { remaining: MAX_TRANSFORM_WORK, matches: 0 };
|
|
260
340
|
}
|
|
341
|
+
|
|
261
342
|
function spend(budget: SnippetTransformBudget, amount: number): void {
|
|
262
343
|
budget.remaining -= amount;
|
|
344
|
+
|
|
263
345
|
if (budget.remaining < 0) {
|
|
264
346
|
throw new RangeError('The snippet transform work budget was exceeded.');
|
|
265
347
|
}
|
|
266
348
|
}
|
|
349
|
+
|
|
267
350
|
export function applySnippetTransform(
|
|
268
351
|
job: SnippetTransformJob,
|
|
269
352
|
maximum = MAX_TRANSFORM_CHARACTERS,
|
|
@@ -273,53 +356,68 @@ export function applySnippetTransform(
|
|
|
273
356
|
expression.lastIndex = 0;
|
|
274
357
|
spend(budget, job.value.length);
|
|
275
358
|
const pieces: string[] = [];
|
|
359
|
+
|
|
276
360
|
let at = 0,
|
|
277
361
|
length = 0,
|
|
278
362
|
matches = 0;
|
|
363
|
+
|
|
279
364
|
const append = (text: string): void => {
|
|
280
365
|
if (text === '') {
|
|
281
366
|
return;
|
|
282
367
|
}
|
|
368
|
+
|
|
283
369
|
length += text.length;
|
|
370
|
+
|
|
284
371
|
if (length > maximum) {
|
|
285
372
|
throw new RangeError('The snippet transform output is too large.');
|
|
286
373
|
}
|
|
374
|
+
|
|
287
375
|
pieces.push(text);
|
|
288
376
|
};
|
|
377
|
+
|
|
289
378
|
const format = (captures: readonly (string | undefined)[]): void => {
|
|
290
379
|
for (const node of job.format) {
|
|
291
380
|
spend(budget, 1);
|
|
381
|
+
|
|
292
382
|
if (node.kind === 'text') {
|
|
293
383
|
append(node.text);
|
|
294
384
|
} else {
|
|
295
385
|
const capture = captures[node.index] ?? '';
|
|
386
|
+
|
|
296
387
|
if (node.kind === 'capture' && node.case !== undefined) {
|
|
297
388
|
spend(budget, capture.length);
|
|
298
389
|
}
|
|
390
|
+
|
|
299
391
|
append(node.kind === 'capture' ? changeCase(capture, node.case) : capture !== '' ? (node.if ?? capture) : (node.else ?? ''));
|
|
300
392
|
}
|
|
301
393
|
}
|
|
302
394
|
};
|
|
395
|
+
|
|
303
396
|
for (let match = expression.exec(job.value); match !== null; match = expression.exec(job.value)) {
|
|
304
397
|
if (++budget.matches > MAX_TRANSFORM_MATCHES) {
|
|
305
398
|
throw new RangeError('The snippet transform batch has too many matches.');
|
|
306
399
|
}
|
|
400
|
+
|
|
307
401
|
matches++;
|
|
308
402
|
spend(budget, 1);
|
|
309
403
|
append(job.value.slice(at, match.index));
|
|
310
404
|
format(match);
|
|
311
405
|
at = match.index + match[0].length;
|
|
406
|
+
|
|
312
407
|
if (!expression.global) {
|
|
313
408
|
break;
|
|
314
409
|
}
|
|
410
|
+
|
|
315
411
|
if (match[0].length === 0) {
|
|
316
412
|
expression.lastIndex += expression.unicode && (job.value.codePointAt(expression.lastIndex) ?? 0) > 0xffff ? 2 : 1;
|
|
317
413
|
}
|
|
318
414
|
}
|
|
415
|
+
|
|
319
416
|
if (matches === 0 && job.format.some((node) => node.kind === 'conditional' && Boolean(node.else))) {
|
|
320
417
|
format([]);
|
|
321
418
|
} else {
|
|
322
419
|
append(job.value.slice(at));
|
|
323
420
|
}
|
|
421
|
+
|
|
324
422
|
return pieces.join('');
|
|
325
423
|
}
|
|
@@ -6,18 +6,23 @@ export const MAX_TRANSFORM_WORK = 1_000_000;
|
|
|
6
6
|
export const MAX_TRANSFORM_PATTERN = 1024;
|
|
7
7
|
export const MAX_TRANSFORM_FORMAT = 10_000;
|
|
8
8
|
export const MAX_TRANSFORM_NODES = 1000;
|
|
9
|
+
|
|
9
10
|
export type SnippetTransformCase = 'upcase' | 'downcase' | 'capitalize' | 'camelcase' | 'pascalcase';
|
|
11
|
+
|
|
10
12
|
export type SnippetTransformFormatNode =
|
|
11
13
|
| { readonly kind: 'text'; readonly text: string }
|
|
12
14
|
| { readonly kind: 'capture'; readonly index: number; readonly case?: SnippetTransformCase }
|
|
13
15
|
| { readonly kind: 'conditional'; readonly index: number; readonly if?: string; readonly else?: string };
|
|
16
|
+
|
|
14
17
|
export interface SnippetTransformJob {
|
|
15
18
|
readonly value: string;
|
|
16
19
|
readonly pattern: string;
|
|
17
20
|
readonly flags: string;
|
|
18
21
|
readonly format: readonly SnippetTransformFormatNode[];
|
|
19
22
|
}
|
|
23
|
+
|
|
20
24
|
export type SnippetTransformInput = { readonly type: 'transform'; readonly id: number; readonly jobs: readonly SnippetTransformJob[] };
|
|
25
|
+
|
|
21
26
|
export type SnippetTransformOutput =
|
|
22
27
|
| { readonly type: 'ready' }
|
|
23
28
|
| { readonly type: 'transformed'; readonly id: number; readonly values: readonly string[] }
|