@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.
Files changed (53) hide show
  1. package/dist/controller.d.ts +1 -0
  2. package/dist/controller.js +17 -28
  3. package/dist/engine.js +2 -9
  4. package/dist/find.js +12 -13
  5. package/dist/layout.js +7 -2
  6. package/dist/paint.d.ts +2 -0
  7. package/dist/paint.js +23 -20
  8. package/dist/view.d.ts +3 -2
  9. package/dist/view.js +46 -59
  10. package/package.json +6 -6
  11. package/src/attribution.ts +14 -0
  12. package/src/code-block.ts +25 -0
  13. package/src/column-selection.ts +10 -0
  14. package/src/controller.ts +189 -28
  15. package/src/engine.ts +87 -9
  16. package/src/fake.ts +79 -0
  17. package/src/find-search.ts +20 -0
  18. package/src/find.ts +135 -14
  19. package/src/geometry-stops.ts +10 -0
  20. package/src/grammar-preparation.ts +23 -0
  21. package/src/index.ts +1 -0
  22. package/src/input.ts +13 -0
  23. package/src/keymap-table.ts +9 -0
  24. package/src/keymap.ts +25 -0
  25. package/src/layout-changes.ts +7 -0
  26. package/src/layout.ts +219 -3
  27. package/src/lens.ts +3 -0
  28. package/src/listeners.ts +1 -0
  29. package/src/metrics.ts +14 -0
  30. package/src/modifier-gesture.ts +17 -0
  31. package/src/native-input.ts +27 -0
  32. package/src/offsets.ts +10 -0
  33. package/src/outline.ts +38 -0
  34. package/src/overview.ts +13 -0
  35. package/src/paint.ts +140 -20
  36. package/src/php-html.ts +1 -0
  37. package/src/pointer.ts +50 -0
  38. package/src/scroll-animation.ts +10 -0
  39. package/src/scroll.ts +7 -0
  40. package/src/search-client.ts +32 -0
  41. package/src/search-worker.ts +38 -0
  42. package/src/semantic.ts +12 -0
  43. package/src/shiki-document.ts +19 -0
  44. package/src/shiki-worker.ts +68 -0
  45. package/src/shiki.ts +37 -0
  46. package/src/testing.ts +17 -0
  47. package/src/theme-scopes.ts +14 -0
  48. package/src/tokens.ts +31 -0
  49. package/src/touch.ts +16 -0
  50. package/src/tracked-range.ts +5 -0
  51. package/src/uniform-geometry.ts +10 -0
  52. package/src/view.ts +481 -75
  53. 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
  }
@@ -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
  }
@@ -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;