@adecore/editor 0.39.0 → 0.40.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/controller.d.ts +1 -0
- package/dist/controller.js +17 -28
- package/dist/engine.js +2 -9
- package/dist/find.js +12 -13
- package/dist/layout.js +7 -2
- package/dist/paint.d.ts +2 -0
- package/dist/paint.js +23 -20
- package/dist/view.d.ts +3 -2
- package/dist/view.js +46 -59
- package/package.json +6 -6
- package/src/attribution.ts +14 -0
- package/src/code-block.ts +25 -0
- package/src/column-selection.ts +10 -0
- package/src/controller.ts +189 -28
- package/src/engine.ts +87 -9
- package/src/fake.ts +79 -0
- package/src/find-search.ts +20 -0
- package/src/find.ts +135 -14
- package/src/geometry-stops.ts +10 -0
- package/src/grammar-preparation.ts +23 -0
- package/src/index.ts +1 -0
- package/src/input.ts +13 -0
- package/src/keymap-table.ts +9 -0
- package/src/keymap.ts +25 -0
- package/src/layout-changes.ts +7 -0
- package/src/layout.ts +219 -3
- package/src/lens.ts +3 -0
- package/src/listeners.ts +1 -0
- package/src/metrics.ts +14 -0
- package/src/modifier-gesture.ts +17 -0
- package/src/native-input.ts +27 -0
- package/src/offsets.ts +10 -0
- package/src/outline.ts +38 -0
- package/src/overview.ts +13 -0
- package/src/paint.ts +140 -20
- package/src/php-html.ts +1 -0
- package/src/pointer.ts +50 -0
- package/src/scroll-animation.ts +10 -0
- package/src/scroll.ts +7 -0
- package/src/search-client.ts +32 -0
- package/src/search-worker.ts +38 -0
- package/src/semantic.ts +12 -0
- package/src/shiki-document.ts +19 -0
- package/src/shiki-worker.ts +68 -0
- package/src/shiki.ts +37 -0
- package/src/testing.ts +17 -0
- package/src/theme-scopes.ts +14 -0
- package/src/tokens.ts +31 -0
- package/src/touch.ts +16 -0
- package/src/tracked-range.ts +5 -0
- package/src/uniform-geometry.ts +10 -0
- package/src/view.ts +481 -75
- package/src/worker-client.ts +54 -0
package/src/semantic.ts
CHANGED
|
@@ -17,27 +17,35 @@ export interface SemanticSpan {
|
|
|
17
17
|
*/
|
|
18
18
|
export function overlayTokens(base: readonly LineToken[], spans: readonly SemanticSpan[]): LineToken[] {
|
|
19
19
|
const result: LineToken[] = [];
|
|
20
|
+
|
|
20
21
|
const push = (length: number, color: string, fontStyle: number): void => {
|
|
21
22
|
if (length <= 0) {
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
25
|
+
|
|
24
26
|
const last = result[result.length - 1];
|
|
27
|
+
|
|
25
28
|
if (last !== undefined && last.color === color && last.fontStyle === fontStyle) {
|
|
26
29
|
result[result.length - 1] = { length: last.length + length, color, fontStyle };
|
|
27
30
|
} else {
|
|
28
31
|
result.push({ length, color, fontStyle });
|
|
29
32
|
}
|
|
30
33
|
};
|
|
34
|
+
|
|
31
35
|
let span = 0;
|
|
32
36
|
let start = 0;
|
|
37
|
+
|
|
33
38
|
for (const token of base) {
|
|
34
39
|
const end = start + token.length;
|
|
35
40
|
let at = start;
|
|
41
|
+
|
|
36
42
|
while (at < end) {
|
|
37
43
|
while (span < spans.length && spans[span]!.to <= at) {
|
|
38
44
|
span++;
|
|
39
45
|
}
|
|
46
|
+
|
|
40
47
|
const current = spans[span];
|
|
48
|
+
|
|
41
49
|
if (current === undefined || current.from >= end) {
|
|
42
50
|
push(end - at, token.color, token.fontStyle);
|
|
43
51
|
at = end;
|
|
@@ -46,15 +54,19 @@ export function overlayTokens(base: readonly LineToken[], spans: readonly Semant
|
|
|
46
54
|
at = current.from;
|
|
47
55
|
} else {
|
|
48
56
|
const stop = Math.min(end, current.to);
|
|
57
|
+
|
|
49
58
|
if (current.within === undefined || token.kind === current.within) {
|
|
50
59
|
push(stop - at, current.color, current.fontStyle);
|
|
51
60
|
} else {
|
|
52
61
|
push(stop - at, token.color, token.fontStyle);
|
|
53
62
|
}
|
|
63
|
+
|
|
54
64
|
at = stop;
|
|
55
65
|
}
|
|
56
66
|
}
|
|
67
|
+
|
|
57
68
|
start = end;
|
|
58
69
|
}
|
|
70
|
+
|
|
59
71
|
return result;
|
|
60
72
|
}
|
package/src/shiki-document.ts
CHANGED
|
@@ -18,6 +18,7 @@ export function shikiDocumentHighlighter(
|
|
|
18
18
|
): (index: number) => Promise<string> {
|
|
19
19
|
return documentHighlighter(highlighter, blocks, language, theme, (loaded, code, grammar, theme, state) => {
|
|
20
20
|
let root: Parameters<NonNullable<ShikiTransformer['root']>>[0] | undefined;
|
|
21
|
+
|
|
21
22
|
const value = loaded.codeToHtml(code, {
|
|
22
23
|
lang: grammar,
|
|
23
24
|
theme,
|
|
@@ -31,6 +32,7 @@ export function shikiDocumentHighlighter(
|
|
|
31
32
|
}
|
|
32
33
|
]
|
|
33
34
|
});
|
|
35
|
+
|
|
34
36
|
return { value, state: root === undefined ? undefined : loaded.getLastGrammarState(root) };
|
|
35
37
|
});
|
|
36
38
|
}
|
|
@@ -56,6 +58,7 @@ export function shikiDocumentTokens(
|
|
|
56
58
|
tokenizeMaxLineLength: options.tokenizeMaxLineLength,
|
|
57
59
|
tokenizeTimeLimit: options.tokenizeTimeLimit
|
|
58
60
|
} as TokenOptions);
|
|
61
|
+
|
|
59
62
|
return { value, state: loaded.getLastGrammarState(value) };
|
|
60
63
|
},
|
|
61
64
|
options.reset
|
|
@@ -88,13 +91,18 @@ function documentHighlighter<Value>(
|
|
|
88
91
|
|
|
89
92
|
const prepare = async (): Promise<{ highlighter: Highlighter; language: string }> => {
|
|
90
93
|
const loaded = await highlighter();
|
|
94
|
+
|
|
91
95
|
if (!loaded.getLoadedThemes().includes(theme)) {
|
|
92
96
|
await loaded.loadTheme(theme as Parameters<Highlighter['loadTheme']>[0]);
|
|
93
97
|
}
|
|
98
|
+
|
|
94
99
|
const documentLanguage = documentLanguageOf(language);
|
|
100
|
+
|
|
95
101
|
const known =
|
|
96
102
|
loaded.getLoadedLanguages().includes(documentLanguage) || documentLanguage in loaded.getBundledLanguages() || documentLanguage !== language;
|
|
103
|
+
|
|
97
104
|
const grammar = known && (await loadGrammar(loaded, documentLanguage, code.join('\n'))) ? documentLanguage : 'text';
|
|
105
|
+
|
|
98
106
|
return { highlighter: loaded, language: grammar };
|
|
99
107
|
};
|
|
100
108
|
|
|
@@ -102,15 +110,20 @@ function documentHighlighter<Value>(
|
|
|
102
110
|
if (!Number.isInteger(index) || index < 0 || index >= code.length) {
|
|
103
111
|
return Promise.reject(new RangeError('The highlighted block index is outside the document.'));
|
|
104
112
|
}
|
|
113
|
+
|
|
105
114
|
const result = pending.then(async () => {
|
|
106
115
|
prepared ??= prepare().catch((error: unknown) => {
|
|
107
116
|
prepared = undefined;
|
|
117
|
+
|
|
108
118
|
throw error;
|
|
109
119
|
});
|
|
120
|
+
|
|
110
121
|
const { highlighter: loaded, language: grammarId } = await prepared;
|
|
122
|
+
|
|
111
123
|
const refresh = (): void => {
|
|
112
124
|
const currentGrammar = grammarId === 'text' ? undefined : loaded.getLanguage(grammarId);
|
|
113
125
|
const currentTheme = loaded.getTheme(theme);
|
|
126
|
+
|
|
114
127
|
// Loading an embedded language rebuilds Shiki's grammar and invalidates its old states.
|
|
115
128
|
if (grammar !== currentGrammar || themeRegistration !== currentTheme) {
|
|
116
129
|
grammar = currentGrammar;
|
|
@@ -119,25 +132,31 @@ function documentHighlighter<Value>(
|
|
|
119
132
|
values = [];
|
|
120
133
|
}
|
|
121
134
|
};
|
|
135
|
+
|
|
122
136
|
refresh();
|
|
137
|
+
|
|
123
138
|
while (values.length <= index) {
|
|
124
139
|
const block = values.length;
|
|
125
140
|
const rendered = render(loaded, code[block]!, grammarId, theme, state);
|
|
126
141
|
state = resets.has(block + 1) ? undefined : rendered.state;
|
|
127
142
|
values.push(rendered.value);
|
|
143
|
+
|
|
128
144
|
if (block < index) {
|
|
129
145
|
// A jump deep into a file must leave room for input and painting between blocks.
|
|
130
146
|
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
131
147
|
refresh();
|
|
132
148
|
}
|
|
133
149
|
}
|
|
150
|
+
|
|
134
151
|
return values[index]!;
|
|
135
152
|
});
|
|
153
|
+
|
|
136
154
|
// A failed theme or grammar load can be retried without rejecting every later request.
|
|
137
155
|
pending = result.then(
|
|
138
156
|
() => undefined,
|
|
139
157
|
() => undefined
|
|
140
158
|
);
|
|
159
|
+
|
|
141
160
|
return result;
|
|
142
161
|
};
|
|
143
162
|
}
|
package/src/shiki-worker.ts
CHANGED
|
@@ -63,11 +63,13 @@ interface Request {
|
|
|
63
63
|
function yieldToHost(): Promise<void> {
|
|
64
64
|
return new Promise((resolve) => {
|
|
65
65
|
const channel = new MessageChannel();
|
|
66
|
+
|
|
66
67
|
channel.port1.onmessage = () => {
|
|
67
68
|
channel.port1.close();
|
|
68
69
|
channel.port2.close();
|
|
69
70
|
resolve();
|
|
70
71
|
};
|
|
72
|
+
|
|
71
73
|
channel.port2.postMessage(null);
|
|
72
74
|
});
|
|
73
75
|
}
|
|
@@ -82,28 +84,36 @@ function packedTokens(
|
|
|
82
84
|
const colors: string[] = [''];
|
|
83
85
|
const colorIds = new Map([['', 0]]);
|
|
84
86
|
const starts = [0];
|
|
87
|
+
|
|
85
88
|
for (const match of code.matchAll(/\r\n|\n/g)) {
|
|
86
89
|
starts.push(match.index + match[0].length);
|
|
87
90
|
}
|
|
91
|
+
|
|
88
92
|
const lines = new Uint32Array(tokens.length + 1);
|
|
89
93
|
const packed = new Uint32Array(tokens.reduce((count, line) => count + line.length, 0) * 3);
|
|
90
94
|
let cursor = 0;
|
|
95
|
+
|
|
91
96
|
for (const [index, line] of tokens.entries()) {
|
|
92
97
|
lines[index] = cursor / 3;
|
|
98
|
+
|
|
93
99
|
for (const token of line) {
|
|
94
100
|
const color = token.color ?? '';
|
|
95
101
|
let colorId = colorIds.get(color);
|
|
102
|
+
|
|
96
103
|
if (colorId === undefined) {
|
|
97
104
|
colorId = colors.length;
|
|
98
105
|
colors.push(color);
|
|
99
106
|
colorIds.set(color, colorId);
|
|
100
107
|
}
|
|
108
|
+
|
|
101
109
|
packed[cursor++] = token.offset - starts[index]!;
|
|
102
110
|
packed[cursor++] = colorId;
|
|
103
111
|
packed[cursor++] = token.fontStyle ?? 0;
|
|
104
112
|
}
|
|
105
113
|
}
|
|
114
|
+
|
|
106
115
|
lines[tokens.length] = cursor / 3;
|
|
116
|
+
|
|
107
117
|
return { type: 'tokens', generation, index, colors, ...appearance, lines, tokens: packed };
|
|
108
118
|
}
|
|
109
119
|
|
|
@@ -111,9 +121,11 @@ function packedTokens(
|
|
|
111
121
|
async function loadMissingThemes(loaded: Highlighter, themes: readonly ThemeRegistration[]): Promise<Highlighter> {
|
|
112
122
|
const known = new Set(loaded.getLoadedThemes());
|
|
113
123
|
const missing = themes.filter((theme) => theme.name === undefined || !known.has(theme.name));
|
|
124
|
+
|
|
114
125
|
if (missing.length > 0) {
|
|
115
126
|
await loaded.loadTheme(...missing);
|
|
116
127
|
}
|
|
128
|
+
|
|
117
129
|
return loaded;
|
|
118
130
|
}
|
|
119
131
|
|
|
@@ -122,15 +134,19 @@ export function lineTokensOf(packet: DiffSyntaxTokens, line: number, text: strin
|
|
|
122
134
|
if (!Number.isInteger(line) || line < 0 || line + 1 >= packet.lines.length) {
|
|
123
135
|
throw new RangeError('Unknown syntax line');
|
|
124
136
|
}
|
|
137
|
+
|
|
125
138
|
const result: LineToken[] = [];
|
|
126
139
|
const end = packet.lines[line + 1]!;
|
|
140
|
+
|
|
127
141
|
for (let index = packet.lines[line]!; index < end; index++) {
|
|
128
142
|
const from = packet.tokens[index * 3]!;
|
|
129
143
|
const to = index + 1 < end ? packet.tokens[(index + 1) * 3]! : text.length;
|
|
144
|
+
|
|
130
145
|
if (to > from) {
|
|
131
146
|
result.push({ length: to - from, color: packet.colors[packet.tokens[index * 3 + 1]!] ?? '', fontStyle: packet.tokens[index * 3 + 2]! });
|
|
132
147
|
}
|
|
133
148
|
}
|
|
149
|
+
|
|
134
150
|
return result;
|
|
135
151
|
}
|
|
136
152
|
|
|
@@ -144,82 +160,106 @@ export function createSyntaxService(options: DiffSyntaxServiceOptions): DiffSynt
|
|
|
144
160
|
let running = false;
|
|
145
161
|
let stopped = false;
|
|
146
162
|
let active: Entry | undefined;
|
|
163
|
+
|
|
147
164
|
const key = (generation: number, index: number): string => `${generation}:${index}`;
|
|
165
|
+
|
|
148
166
|
const cancel = (generation: number): void => {
|
|
149
167
|
for (const [id, request] of requests) {
|
|
150
168
|
if (request.generation === generation) {
|
|
151
169
|
requests.delete(id);
|
|
170
|
+
|
|
152
171
|
for (const resolve of request.resolve) {
|
|
153
172
|
resolve();
|
|
154
173
|
}
|
|
155
174
|
}
|
|
156
175
|
}
|
|
157
176
|
};
|
|
177
|
+
|
|
158
178
|
const trim = (): void => {
|
|
159
179
|
for (const [generation, entry] of entries) {
|
|
160
180
|
if (count <= maxLines) {
|
|
161
181
|
break;
|
|
162
182
|
}
|
|
183
|
+
|
|
163
184
|
if (entry !== active) {
|
|
164
185
|
entries.delete(generation);
|
|
165
186
|
count -= entry.count;
|
|
166
187
|
}
|
|
167
188
|
}
|
|
168
189
|
};
|
|
190
|
+
|
|
169
191
|
const finish = (request: Request): void => {
|
|
170
192
|
const id = key(request.generation, request.index);
|
|
193
|
+
|
|
171
194
|
if (requests.get(id) === request) {
|
|
172
195
|
requests.delete(id);
|
|
173
196
|
}
|
|
197
|
+
|
|
174
198
|
for (const resolve of request.resolve) {
|
|
175
199
|
resolve();
|
|
176
200
|
}
|
|
177
201
|
};
|
|
202
|
+
|
|
178
203
|
const failed = (request: Request, reason: 'missing' | 'highlight', message: string): void => {
|
|
179
204
|
options.send({ type: 'failed', generation: request.generation, index: request.index, reason, message });
|
|
180
205
|
finish(request);
|
|
181
206
|
};
|
|
207
|
+
|
|
182
208
|
const grammarOf = (entry: Entry, loaded: Highlighter): unknown => {
|
|
183
209
|
const language = documentLanguageOf(entry.document.language);
|
|
210
|
+
|
|
184
211
|
return loaded.getLoadedLanguages().includes(language) ? loaded.getLanguage(language) : undefined;
|
|
185
212
|
};
|
|
213
|
+
|
|
186
214
|
const pump = async (): Promise<void> => {
|
|
187
215
|
if (running) {
|
|
188
216
|
return;
|
|
189
217
|
}
|
|
218
|
+
|
|
190
219
|
running = true;
|
|
220
|
+
|
|
191
221
|
try {
|
|
192
222
|
while (!stopped && requests.size > 0) {
|
|
193
223
|
const request = [...requests.values()].at(-1)!;
|
|
194
224
|
const id = key(request.generation, request.index);
|
|
195
225
|
const entry = entries.get(request.generation);
|
|
226
|
+
|
|
196
227
|
if (entry === undefined) {
|
|
197
228
|
failed(request, 'missing', 'The syntax document was evicted; resend its definition.');
|
|
229
|
+
|
|
198
230
|
continue;
|
|
199
231
|
}
|
|
232
|
+
|
|
200
233
|
active = entry;
|
|
201
234
|
entries.delete(request.generation);
|
|
202
235
|
entries.set(request.generation, entry);
|
|
236
|
+
|
|
203
237
|
try {
|
|
204
238
|
const loaded = await (ready ??= options.highlighter());
|
|
239
|
+
|
|
205
240
|
if (requests.get(id) !== request || entries.get(request.generation) !== entry || stopped) {
|
|
206
241
|
continue;
|
|
207
242
|
}
|
|
243
|
+
|
|
208
244
|
if (entry.done >= 0 && (entry.grammar !== grammarOf(entry, loaded) || entry.theme !== loaded.getTheme(entry.document.theme))) {
|
|
209
245
|
entry.done = -1;
|
|
210
246
|
}
|
|
247
|
+
|
|
211
248
|
if (request.index >= entry.document.blocks.length) {
|
|
212
249
|
throw new RangeError('The syntax block index is outside the document.');
|
|
213
250
|
}
|
|
251
|
+
|
|
214
252
|
if ([...requests.values()].at(-1) !== request) {
|
|
215
253
|
continue;
|
|
216
254
|
}
|
|
255
|
+
|
|
217
256
|
const index = Math.min(entry.done + 1, request.index);
|
|
218
257
|
const tokens = await entry.tokens(index);
|
|
219
258
|
entry.done = Math.max(entry.done, index);
|
|
220
259
|
entry.grammar = grammarOf(entry, loaded);
|
|
221
260
|
const theme = loaded.getTheme(entry.document.theme);
|
|
222
261
|
entry.theme = theme;
|
|
262
|
+
|
|
223
263
|
if (requests.get(id) === request && entries.get(request.generation) === entry && !stopped && index === request.index) {
|
|
224
264
|
const raw = entry.document.blocks[index]!;
|
|
225
265
|
const code = index < entry.document.blocks.length - 1 ? raw.replace(/\r$/, '') : raw;
|
|
@@ -232,7 +272,9 @@ export function createSyntaxService(options: DiffSyntaxServiceOptions): DiffSynt
|
|
|
232
272
|
failed(request, 'highlight', error instanceof Error ? error.message : String(error));
|
|
233
273
|
}
|
|
234
274
|
}
|
|
275
|
+
|
|
235
276
|
trim();
|
|
277
|
+
|
|
236
278
|
if (requests.size > 0 && !stopped) {
|
|
237
279
|
await pause();
|
|
238
280
|
}
|
|
@@ -243,67 +285,86 @@ export function createSyntaxService(options: DiffSyntaxServiceOptions): DiffSynt
|
|
|
243
285
|
trim();
|
|
244
286
|
}
|
|
245
287
|
};
|
|
288
|
+
|
|
246
289
|
const dispose = (): void => {
|
|
247
290
|
stopped = true;
|
|
291
|
+
|
|
248
292
|
for (const generation of entries.keys()) {
|
|
249
293
|
cancel(generation);
|
|
250
294
|
}
|
|
295
|
+
|
|
251
296
|
for (const request of requests.values()) {
|
|
252
297
|
finish(request);
|
|
253
298
|
}
|
|
299
|
+
|
|
254
300
|
entries.clear();
|
|
255
301
|
count = 0;
|
|
256
302
|
};
|
|
303
|
+
|
|
257
304
|
return {
|
|
258
305
|
dispose,
|
|
259
306
|
handle(message) {
|
|
260
307
|
if (stopped) {
|
|
261
308
|
return Promise.resolve();
|
|
262
309
|
}
|
|
310
|
+
|
|
263
311
|
if (message.type === 'start') {
|
|
264
312
|
if (!Number.isSafeInteger(message.maxLines) || message.maxLines < 1) {
|
|
265
313
|
return Promise.reject(new RangeError('The syntax cache limit must be a positive safe integer.'));
|
|
266
314
|
}
|
|
315
|
+
|
|
267
316
|
maxLines = message.maxLines;
|
|
317
|
+
|
|
268
318
|
for (const generation of entries.keys()) {
|
|
269
319
|
cancel(generation);
|
|
270
320
|
}
|
|
321
|
+
|
|
271
322
|
entries.clear();
|
|
272
323
|
count = 0;
|
|
273
324
|
ready = options.highlighter().then((loaded) => loadMissingThemes(loaded, message.themes));
|
|
325
|
+
|
|
274
326
|
return ready.then(
|
|
275
327
|
() => options.send({ type: 'ready' }),
|
|
276
328
|
(error: unknown) => options.send({ type: 'error', message: error instanceof Error ? error.message : String(error) })
|
|
277
329
|
);
|
|
278
330
|
}
|
|
331
|
+
|
|
279
332
|
if (message.type === 'themes') {
|
|
280
333
|
// The queue awaits `ready` anew for every request, so a request sent after this one draws in these themes.
|
|
281
334
|
ready = (ready ?? options.highlighter()).then((loaded) =>
|
|
282
335
|
// A theme that fails to load fails only the requests drawn in it, never the ones in themes already in.
|
|
283
336
|
loadMissingThemes(loaded, message.themes).catch(() => loaded)
|
|
284
337
|
);
|
|
338
|
+
|
|
285
339
|
// A failed start has already reported its error.
|
|
286
340
|
return ready.then(
|
|
287
341
|
() => undefined,
|
|
288
342
|
() => undefined
|
|
289
343
|
);
|
|
290
344
|
}
|
|
345
|
+
|
|
291
346
|
if (message.type === 'cancel' || message.type === 'dispose') {
|
|
292
347
|
cancel(message.generation);
|
|
348
|
+
|
|
293
349
|
if (message.type === 'dispose') {
|
|
294
350
|
const entry = entries.get(message.generation);
|
|
351
|
+
|
|
295
352
|
if (entry !== undefined) {
|
|
296
353
|
entries.delete(message.generation);
|
|
297
354
|
count -= entry.count;
|
|
298
355
|
}
|
|
299
356
|
}
|
|
357
|
+
|
|
300
358
|
return Promise.resolve();
|
|
301
359
|
}
|
|
360
|
+
|
|
302
361
|
if (!Number.isSafeInteger(message.generation) || message.generation < 1 || !Number.isSafeInteger(message.index) || message.index < 0) {
|
|
303
362
|
return Promise.reject(new RangeError('Syntax generations must be positive integers and block indexes nonnegative integers.'));
|
|
304
363
|
}
|
|
364
|
+
|
|
305
365
|
if (message.document !== undefined && !entries.has(message.generation)) {
|
|
306
366
|
const document = { ...message.document, blocks: [...message.document.blocks], resets: [...message.document.resets] };
|
|
367
|
+
|
|
307
368
|
const entry: Entry = {
|
|
308
369
|
document,
|
|
309
370
|
tokens: shikiDocumentTokens(() => (ready ??= options.highlighter()), document.blocks, document.language, document.theme, {
|
|
@@ -315,9 +376,11 @@ export function createSyntaxService(options: DiffSyntaxServiceOptions): DiffSynt
|
|
|
315
376
|
count: document.blocks.reduce((count, block) => count + block.split(/\r\n|\n/).length, 0),
|
|
316
377
|
done: -1
|
|
317
378
|
};
|
|
379
|
+
|
|
318
380
|
count += entry.count;
|
|
319
381
|
entries.set(message.generation, entry);
|
|
320
382
|
}
|
|
383
|
+
|
|
321
384
|
return new Promise((resolve) => {
|
|
322
385
|
const id = key(message.generation, message.index);
|
|
323
386
|
const existing = requests.get(id);
|
|
@@ -339,19 +402,24 @@ export interface DiffSyntaxWorkerEndpoint {
|
|
|
339
402
|
|
|
340
403
|
export function serveDiffSyntax(endpoint: DiffSyntaxWorkerEndpoint): () => void {
|
|
341
404
|
let highlighter: Promise<Highlighter> | undefined;
|
|
405
|
+
|
|
342
406
|
const service = createSyntaxService({
|
|
343
407
|
highlighter: () => (highlighter ??= import('shiki').then(({ createHighlighter }) => createHighlighter({ langs: [], themes: [] }))),
|
|
344
408
|
send: (message, transfer) => endpoint.postMessage(message, { transfer: transfer ?? [] })
|
|
345
409
|
});
|
|
410
|
+
|
|
346
411
|
const listener = (event: MessageEvent<DiffSyntaxInput>): void => {
|
|
347
412
|
void service.handle(event.data).catch((error: unknown) => {
|
|
348
413
|
endpoint.postMessage({ type: 'error', message: error instanceof Error ? error.message : String(error) });
|
|
349
414
|
});
|
|
350
415
|
};
|
|
416
|
+
|
|
351
417
|
endpoint.addEventListener('message', listener);
|
|
418
|
+
|
|
352
419
|
return () => {
|
|
353
420
|
endpoint.removeEventListener('message', listener);
|
|
354
421
|
service.dispose();
|
|
422
|
+
|
|
355
423
|
void highlighter?.then(
|
|
356
424
|
(loaded) => loaded.dispose(),
|
|
357
425
|
() => undefined
|
package/src/shiki.ts
CHANGED
|
@@ -17,12 +17,14 @@ const PLAIN_TEXT = new Set(['', 'text', 'txt', 'plain', 'plaintext']);
|
|
|
17
17
|
/* Colors one line at a time and hands the grammar's state from one line to the next. A grammar Shiki rebuilt does not know the states of the old one. */
|
|
18
18
|
function lineTokenizer(highlighter: Highlighter, language: string, theme: string, embedded: EmbeddedIndex): LineTokenizer {
|
|
19
19
|
let grammar = highlighter.getLanguage(language);
|
|
20
|
+
|
|
20
21
|
return {
|
|
21
22
|
prepare: embedded.matcher === null ? undefined : (text) => loadEmbedded(highlighter, embedded, text),
|
|
22
23
|
stale() {
|
|
23
24
|
const current = highlighter.getLanguage(language);
|
|
24
25
|
const changed = current !== grammar;
|
|
25
26
|
grammar = current;
|
|
27
|
+
|
|
26
28
|
return changed;
|
|
27
29
|
},
|
|
28
30
|
tokenizeLine(text, state) {
|
|
@@ -32,21 +34,26 @@ function lineTokenizer(highlighter: Highlighter, language: string, theme: string
|
|
|
32
34
|
grammarState: (state ?? undefined) as GrammarState | undefined,
|
|
33
35
|
includeExplanation: 'tokenType'
|
|
34
36
|
} as TokenOptions;
|
|
37
|
+
|
|
35
38
|
const lines = highlighter.codeToTokensBase(text, options);
|
|
39
|
+
|
|
36
40
|
const tokens: LineToken[] = (lines[0] ?? []).map((token) => ({
|
|
37
41
|
length: token.content.length,
|
|
38
42
|
color: token.color ?? '',
|
|
39
43
|
fontStyle: token.fontStyle ?? 0,
|
|
40
44
|
kind: KINDS[token.type ?? 0]
|
|
41
45
|
}));
|
|
46
|
+
|
|
42
47
|
return { tokens, state: highlighter.getLastGrammarState(lines) };
|
|
43
48
|
},
|
|
44
49
|
sameState(a, b) {
|
|
45
50
|
if (a === b) {
|
|
46
51
|
return true;
|
|
47
52
|
}
|
|
53
|
+
|
|
48
54
|
const left = (a as GrammarState | null)?.getInternalStack(theme);
|
|
49
55
|
const right = (b as GrammarState | null)?.getInternalStack(theme);
|
|
56
|
+
|
|
50
57
|
return left !== undefined && right !== undefined && left.equals(right);
|
|
51
58
|
}
|
|
52
59
|
};
|
|
@@ -65,51 +72,67 @@ function loadOnce(highlighter: Highlighter, id: string): Promise<void> {
|
|
|
65
72
|
if (highlighter.getLoadedLanguages().includes(id)) {
|
|
66
73
|
return Promise.resolve();
|
|
67
74
|
}
|
|
75
|
+
|
|
68
76
|
let loads = languageLoads.get(highlighter);
|
|
77
|
+
|
|
69
78
|
if (loads === undefined) {
|
|
70
79
|
loads = new Map();
|
|
71
80
|
languageLoads.set(highlighter, loads);
|
|
72
81
|
}
|
|
82
|
+
|
|
73
83
|
const pending = loads.get(id);
|
|
84
|
+
|
|
74
85
|
if (pending !== undefined) {
|
|
75
86
|
return pending;
|
|
76
87
|
}
|
|
88
|
+
|
|
77
89
|
const load = (async () => {
|
|
78
90
|
await loadDocumentLanguage(highlighter, id);
|
|
91
|
+
|
|
79
92
|
if (!highlighter.getLoadedLanguages().includes(id)) {
|
|
80
93
|
await highlighter.loadLanguage(id as LanguageId);
|
|
81
94
|
}
|
|
82
95
|
})();
|
|
96
|
+
|
|
83
97
|
loads.set(id, load);
|
|
98
|
+
|
|
84
99
|
void load.catch(() => {
|
|
85
100
|
if (loads.get(id) === load) {
|
|
86
101
|
loads.delete(id);
|
|
87
102
|
}
|
|
88
103
|
});
|
|
104
|
+
|
|
89
105
|
return load;
|
|
90
106
|
}
|
|
91
107
|
|
|
92
108
|
function embedIndex(grammar: string): Promise<EmbeddedIndex> {
|
|
93
109
|
const cached = embeddedIndexes.get(grammar);
|
|
110
|
+
|
|
94
111
|
if (cached !== undefined) {
|
|
95
112
|
return cached;
|
|
96
113
|
}
|
|
114
|
+
|
|
97
115
|
const pending = (async (): Promise<EmbeddedIndex> => {
|
|
98
116
|
const { bundledLanguages, bundledLanguagesInfo } = await import('shiki');
|
|
99
117
|
const bundled = bundledLanguages[grammar as keyof typeof bundledLanguages];
|
|
100
118
|
const registrations = bundled === undefined ? [] : (await bundled()).default;
|
|
101
119
|
const lazy = registrations.find((registration) => registration.name === grammar)?.embeddedLangsLazy ?? [];
|
|
102
120
|
const names = new Map<string, string>();
|
|
121
|
+
|
|
103
122
|
for (const id of lazy) {
|
|
104
123
|
const aliases = bundledLanguagesInfo.find((info) => info.id === id)?.aliases ?? [];
|
|
124
|
+
|
|
105
125
|
for (const name of [id, ...aliases]) {
|
|
106
126
|
names.set(name.toLowerCase(), id);
|
|
107
127
|
}
|
|
108
128
|
}
|
|
129
|
+
|
|
109
130
|
if (names.size === 0) {
|
|
110
131
|
return { names, matcher: null };
|
|
111
132
|
}
|
|
133
|
+
|
|
112
134
|
let matcher: RegExp;
|
|
135
|
+
|
|
113
136
|
if (grammar === 'markdown' || grammar === 'mdx') {
|
|
114
137
|
matcher = /^\s*(?:`{3,}|~{3,})[ \t]*([\w#+.-]+)/gm;
|
|
115
138
|
} else if (grammar === 'vue' || grammar === 'svelte' || grammar === 'astro') {
|
|
@@ -118,10 +141,13 @@ function embedIndex(grammar: string): Promise<EmbeddedIndex> {
|
|
|
118
141
|
const alternatives = [...names.keys()].map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
119
142
|
matcher = new RegExp(`(?<![\\w-])(${alternatives})(?![\\w-])`, 'gi');
|
|
120
143
|
}
|
|
144
|
+
|
|
121
145
|
return { names, matcher };
|
|
122
146
|
})();
|
|
147
|
+
|
|
123
148
|
embeddedIndexes.set(grammar, pending);
|
|
124
149
|
void pending.catch(() => embeddedIndexes.delete(grammar));
|
|
150
|
+
|
|
125
151
|
return pending;
|
|
126
152
|
}
|
|
127
153
|
|
|
@@ -129,15 +155,20 @@ async function loadEmbedded(highlighter: Highlighter, embedded: EmbeddedIndex, t
|
|
|
129
155
|
if (embedded.matcher === null) {
|
|
130
156
|
return false;
|
|
131
157
|
}
|
|
158
|
+
|
|
132
159
|
const loaded = new Set(highlighter.getLoadedLanguages());
|
|
133
160
|
const named = new Set<string>();
|
|
161
|
+
|
|
134
162
|
for (const match of text.matchAll(embedded.matcher)) {
|
|
135
163
|
const id = embedded.names.get(match[1]!.toLowerCase());
|
|
164
|
+
|
|
136
165
|
if (id !== undefined && !loaded.has(id)) {
|
|
137
166
|
named.add(id);
|
|
138
167
|
}
|
|
139
168
|
}
|
|
169
|
+
|
|
140
170
|
const results = await Promise.allSettled([...named].map((id) => loadOnce(highlighter, id)));
|
|
171
|
+
|
|
141
172
|
return results.some((result) => result.status === 'fulfilled');
|
|
142
173
|
}
|
|
143
174
|
|
|
@@ -145,6 +176,7 @@ async function loadEmbedded(highlighter: Highlighter, embedded: EmbeddedIndex, t
|
|
|
145
176
|
export async function loadGrammar(highlighter: Highlighter, grammar: string, text = ''): Promise<boolean> {
|
|
146
177
|
await loadOnce(highlighter, grammar);
|
|
147
178
|
await loadEmbedded(highlighter, await embedIndex(grammar), text);
|
|
179
|
+
|
|
148
180
|
return highlighter.getLoadedLanguages().includes(grammar);
|
|
149
181
|
}
|
|
150
182
|
|
|
@@ -157,12 +189,15 @@ export function shikiTokenizers(highlighter: () => Promise<Highlighter>): Tokeni
|
|
|
157
189
|
if (language === undefined || PLAIN_TEXT.has(language)) {
|
|
158
190
|
return null;
|
|
159
191
|
}
|
|
192
|
+
|
|
160
193
|
const grammar = documentLanguageOf(language);
|
|
161
194
|
const shiki = await highlighter();
|
|
195
|
+
|
|
162
196
|
try {
|
|
163
197
|
if (!shiki.getLoadedThemes().includes(theme)) {
|
|
164
198
|
await shiki.loadTheme(theme as ThemeId);
|
|
165
199
|
}
|
|
200
|
+
|
|
166
201
|
return (await loadGrammar(shiki, grammar, text)) ? lineTokenizer(shiki, grammar, theme, await embedIndex(grammar)) : null;
|
|
167
202
|
} catch {
|
|
168
203
|
return null;
|
|
@@ -174,10 +209,12 @@ export function shikiTokenizers(highlighter: () => Promise<Highlighter>): Tokeni
|
|
|
174
209
|
export function shikiScopeColors(highlighter: () => Promise<Highlighter>): ScopeColorSource {
|
|
175
210
|
return async (theme) => {
|
|
176
211
|
const shiki = await highlighter();
|
|
212
|
+
|
|
177
213
|
try {
|
|
178
214
|
if (!shiki.getLoadedThemes().includes(theme)) {
|
|
179
215
|
await shiki.loadTheme(theme as ThemeId);
|
|
180
216
|
}
|
|
217
|
+
|
|
181
218
|
return scopeColorsOf(shiki.getTheme(theme).settings as readonly ThemeRule[]);
|
|
182
219
|
} catch {
|
|
183
220
|
return null;
|