@adecore/editor 0.22.1 → 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.
Files changed (94) hide show
  1. package/dist/attribution.d.ts +9 -8
  2. package/dist/attribution.js +44 -9
  3. package/dist/bidi.d.ts +44 -0
  4. package/dist/bidi.js +26 -0
  5. package/dist/column-selection.js +2 -1
  6. package/dist/controller.d.ts +6 -1
  7. package/dist/controller.js +93 -3
  8. package/dist/editor.css +81 -1
  9. package/dist/engine.js +184 -41
  10. package/dist/fake.d.ts +43 -5
  11. package/dist/fake.js +117 -13
  12. package/dist/find-search.d.ts +26 -0
  13. package/dist/find-search.js +130 -0
  14. package/dist/find.d.ts +50 -2
  15. package/dist/find.js +575 -25
  16. package/dist/geometry-stops.d.ts +6 -0
  17. package/dist/geometry-stops.js +47 -0
  18. package/dist/grammar-preparation.d.ts +29 -0
  19. package/dist/grammar-preparation.js +147 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.js +2 -0
  22. package/dist/keymap-table.d.ts +10 -1
  23. package/dist/keymap-table.js +37 -1
  24. package/dist/keymap.d.ts +1 -0
  25. package/dist/keymap.js +4 -1
  26. package/dist/layout-changes.d.ts +6 -0
  27. package/dist/layout-changes.js +27 -0
  28. package/dist/layout.d.ts +45 -2
  29. package/dist/layout.js +202 -58
  30. package/dist/offsets.d.ts +1 -0
  31. package/dist/offsets.js +23 -0
  32. package/dist/outline.js +5 -4
  33. package/dist/paint.d.ts +11 -3
  34. package/dist/paint.js +180 -34
  35. package/dist/search-client.d.ts +27 -0
  36. package/dist/search-client.js +210 -0
  37. package/dist/search-protocol.d.ts +103 -0
  38. package/dist/search-protocol.js +3 -0
  39. package/dist/search-worker.d.ts +11 -0
  40. package/dist/search-worker.js +137 -0
  41. package/dist/semantic.d.ts +2 -1
  42. package/dist/semantic.js +6 -1
  43. package/dist/shiki-document.d.ts +8 -0
  44. package/dist/shiki-document.js +93 -0
  45. package/dist/shiki-worker.d.ts +65 -0
  46. package/dist/shiki-worker.js +265 -0
  47. package/dist/shiki.d.ts +1 -0
  48. package/dist/shiki.js +96 -33
  49. package/dist/testing.d.ts +1 -0
  50. package/dist/tokens.d.ts +2 -0
  51. package/dist/tokens.js +37 -7
  52. package/dist/touch.d.ts +27 -0
  53. package/dist/touch.js +100 -0
  54. package/dist/types.d.ts +84 -2
  55. package/dist/uniform-geometry.d.ts +7 -0
  56. package/dist/uniform-geometry.js +49 -0
  57. package/dist/view.d.ts +68 -17
  58. package/dist/view.js +424 -97
  59. package/dist/worker-client.d.ts +37 -0
  60. package/dist/worker-client.js +195 -0
  61. package/package.json +17 -2
  62. package/src/attribution.ts +50 -14
  63. package/src/bidi.ts +32 -0
  64. package/src/column-selection.ts +2 -1
  65. package/src/controller.ts +97 -3
  66. package/src/editor.css +81 -1
  67. package/src/engine.ts +216 -42
  68. package/src/fake.ts +152 -14
  69. package/src/find-search.ts +165 -0
  70. package/src/find.ts +622 -21
  71. package/src/geometry-stops.ts +51 -0
  72. package/src/grammar-preparation.ts +158 -0
  73. package/src/index.ts +10 -0
  74. package/src/keymap-table.ts +44 -2
  75. package/src/keymap.ts +5 -1
  76. package/src/layout-changes.ts +33 -0
  77. package/src/layout.ts +262 -63
  78. package/src/offsets.ts +24 -0
  79. package/src/outline.ts +5 -4
  80. package/src/paint.ts +202 -36
  81. package/src/search-client.ts +306 -0
  82. package/src/search-protocol.ts +100 -0
  83. package/src/search-worker.ts +171 -0
  84. package/src/semantic.ts +8 -2
  85. package/src/shiki-document.ts +143 -0
  86. package/src/shiki-worker.ts +344 -0
  87. package/src/shiki.ts +103 -33
  88. package/src/testing.ts +2 -0
  89. package/src/tokens.ts +36 -7
  90. package/src/touch.ts +134 -0
  91. package/src/types.ts +174 -8
  92. package/src/uniform-geometry.ts +57 -0
  93. package/src/view.ts +500 -102
  94. package/src/worker-client.ts +212 -0
@@ -0,0 +1,265 @@
1
+ import { shikiDocumentTokens } from './shiki-document.js';
2
+ import { documentLanguageOf } from './shiki.js';
3
+ function yieldToHost() {
4
+ return new Promise((resolve) => {
5
+ const channel = new MessageChannel();
6
+ channel.port1.onmessage = () => {
7
+ channel.port1.close();
8
+ channel.port2.close();
9
+ resolve();
10
+ };
11
+ channel.port2.postMessage(null);
12
+ });
13
+ }
14
+ function packedTokens(generation, index, code, tokens, appearance) {
15
+ const colors = [''];
16
+ const colorIds = new Map([['', 0]]);
17
+ const starts = [0];
18
+ for (const match of code.matchAll(/\r\n|\n/g)) {
19
+ starts.push(match.index + match[0].length);
20
+ }
21
+ const lines = new Uint32Array(tokens.length + 1);
22
+ const packed = new Uint32Array(tokens.reduce((count, line) => count + line.length, 0) * 3);
23
+ let cursor = 0;
24
+ for (const [index, line] of tokens.entries()) {
25
+ lines[index] = cursor / 3;
26
+ for (const token of line) {
27
+ const color = token.color ?? '';
28
+ let colorId = colorIds.get(color);
29
+ if (colorId === undefined) {
30
+ colorId = colors.length;
31
+ colors.push(color);
32
+ colorIds.set(color, colorId);
33
+ }
34
+ packed[cursor++] = token.offset - starts[index];
35
+ packed[cursor++] = colorId;
36
+ packed[cursor++] = token.fontStyle ?? 0;
37
+ }
38
+ }
39
+ lines[tokens.length] = cursor / 3;
40
+ return { type: 'tokens', generation, index, colors, ...appearance, lines, tokens: packed };
41
+ }
42
+ /* The packet omits text and token lengths; its source line supplies the final token's end. */
43
+ export function lineTokensOf(packet, line, text) {
44
+ if (!Number.isInteger(line) || line < 0 || line + 1 >= packet.lines.length) {
45
+ throw new RangeError('Unknown syntax line');
46
+ }
47
+ const result = [];
48
+ const end = packet.lines[line + 1];
49
+ for (let index = packet.lines[line]; index < end; index++) {
50
+ const from = packet.tokens[index * 3];
51
+ const to = index + 1 < end ? packet.tokens[(index + 1) * 3] : text.length;
52
+ if (to > from) {
53
+ result.push({ length: to - from, color: packet.colors[packet.tokens[index * 3 + 1]] ?? '', fontStyle: packet.tokens[index * 3 + 2] });
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+ export function createSyntaxService(options) {
59
+ const entries = new Map();
60
+ const requests = new Map();
61
+ const pause = options.yield ?? yieldToHost;
62
+ let maxLines = 100000;
63
+ let count = 0;
64
+ let ready;
65
+ let running = false;
66
+ let stopped = false;
67
+ let active;
68
+ const key = (generation, index) => `${generation}:${index}`;
69
+ const cancel = (generation) => {
70
+ for (const [id, request] of requests) {
71
+ if (request.generation === generation) {
72
+ requests.delete(id);
73
+ for (const resolve of request.resolve) {
74
+ resolve();
75
+ }
76
+ }
77
+ }
78
+ };
79
+ const trim = () => {
80
+ for (const [generation, entry] of entries) {
81
+ if (count <= maxLines) {
82
+ break;
83
+ }
84
+ if (entry !== active) {
85
+ entries.delete(generation);
86
+ count -= entry.count;
87
+ }
88
+ }
89
+ };
90
+ const finish = (request) => {
91
+ const id = key(request.generation, request.index);
92
+ if (requests.get(id) === request) {
93
+ requests.delete(id);
94
+ }
95
+ for (const resolve of request.resolve) {
96
+ resolve();
97
+ }
98
+ };
99
+ const failed = (request, reason, message) => {
100
+ options.send({ type: 'failed', generation: request.generation, index: request.index, reason, message });
101
+ finish(request);
102
+ };
103
+ const grammarOf = (entry, loaded) => {
104
+ const language = documentLanguageOf(entry.document.language);
105
+ return loaded.getLoadedLanguages().includes(language) ? loaded.getLanguage(language) : undefined;
106
+ };
107
+ const pump = async () => {
108
+ if (running) {
109
+ return;
110
+ }
111
+ running = true;
112
+ try {
113
+ while (!stopped && requests.size > 0) {
114
+ const request = [...requests.values()].at(-1);
115
+ const id = key(request.generation, request.index);
116
+ const entry = entries.get(request.generation);
117
+ if (entry === undefined) {
118
+ failed(request, 'missing', 'The syntax document was evicted; resend its definition.');
119
+ continue;
120
+ }
121
+ active = entry;
122
+ entries.delete(request.generation);
123
+ entries.set(request.generation, entry);
124
+ try {
125
+ const loaded = await (ready ??= options.highlighter());
126
+ if (requests.get(id) !== request || entries.get(request.generation) !== entry || stopped) {
127
+ continue;
128
+ }
129
+ if (entry.done >= 0 && (entry.grammar !== grammarOf(entry, loaded) || entry.theme !== loaded.getTheme(entry.document.theme))) {
130
+ entry.done = -1;
131
+ }
132
+ if (request.index >= entry.document.blocks.length) {
133
+ throw new RangeError('The syntax block index is outside the document.');
134
+ }
135
+ if ([...requests.values()].at(-1) !== request) {
136
+ continue;
137
+ }
138
+ const index = Math.min(entry.done + 1, request.index);
139
+ const tokens = await entry.tokens(index);
140
+ entry.done = Math.max(entry.done, index);
141
+ entry.grammar = grammarOf(entry, loaded);
142
+ const theme = loaded.getTheme(entry.document.theme);
143
+ entry.theme = theme;
144
+ if (requests.get(id) === request && entries.get(request.generation) === entry && !stopped && index === request.index) {
145
+ const raw = entry.document.blocks[index];
146
+ const code = index < entry.document.blocks.length - 1 ? raw.replace(/\r$/, '') : raw;
147
+ const packed = packedTokens(request.generation, index, code, tokens, { foreground: theme.fg, background: theme.bg });
148
+ options.send(packed, [packed.lines.buffer, packed.tokens.buffer]);
149
+ finish(request);
150
+ }
151
+ }
152
+ catch (error) {
153
+ if (requests.get(id) === request && !stopped) {
154
+ failed(request, 'highlight', error instanceof Error ? error.message : String(error));
155
+ }
156
+ }
157
+ trim();
158
+ if (requests.size > 0 && !stopped) {
159
+ await pause();
160
+ }
161
+ }
162
+ }
163
+ finally {
164
+ active = undefined;
165
+ running = false;
166
+ trim();
167
+ }
168
+ };
169
+ const dispose = () => {
170
+ stopped = true;
171
+ for (const generation of entries.keys()) {
172
+ cancel(generation);
173
+ }
174
+ for (const request of requests.values()) {
175
+ finish(request);
176
+ }
177
+ entries.clear();
178
+ count = 0;
179
+ };
180
+ return {
181
+ dispose,
182
+ handle(message) {
183
+ if (stopped) {
184
+ return Promise.resolve();
185
+ }
186
+ if (message.type === 'start') {
187
+ if (!Number.isSafeInteger(message.maxLines) || message.maxLines < 1) {
188
+ return Promise.reject(new RangeError('The syntax cache limit must be a positive safe integer.'));
189
+ }
190
+ maxLines = message.maxLines;
191
+ for (const generation of entries.keys()) {
192
+ cancel(generation);
193
+ }
194
+ entries.clear();
195
+ count = 0;
196
+ ready = options.highlighter().then(async (loaded) => {
197
+ const known = new Set(loaded.getLoadedThemes());
198
+ const missing = message.themes.filter((theme) => theme.name === undefined || !known.has(theme.name));
199
+ if (missing.length > 0) {
200
+ await loaded.loadTheme(...missing);
201
+ }
202
+ return loaded;
203
+ });
204
+ return ready.then(() => options.send({ type: 'ready' }), (error) => options.send({ type: 'error', message: error instanceof Error ? error.message : String(error) }));
205
+ }
206
+ if (message.type === 'cancel' || message.type === 'dispose') {
207
+ cancel(message.generation);
208
+ if (message.type === 'dispose') {
209
+ const entry = entries.get(message.generation);
210
+ if (entry !== undefined) {
211
+ entries.delete(message.generation);
212
+ count -= entry.count;
213
+ }
214
+ }
215
+ return Promise.resolve();
216
+ }
217
+ if (!Number.isSafeInteger(message.generation) || message.generation < 1 || !Number.isSafeInteger(message.index) || message.index < 0) {
218
+ return Promise.reject(new RangeError('Syntax generations must be positive integers and block indexes nonnegative integers.'));
219
+ }
220
+ if (message.document !== undefined && !entries.has(message.generation)) {
221
+ const document = { ...message.document, blocks: [...message.document.blocks], resets: [...message.document.resets] };
222
+ const entry = {
223
+ document,
224
+ tokens: shikiDocumentTokens(() => (ready ??= options.highlighter()), document.blocks, document.language, document.theme, {
225
+ reset: new Set(document.resets),
226
+ // Shiki skips at the cutoff; include source lines of exactly 10,000 code units.
227
+ tokenizeMaxLineLength: 10001,
228
+ tokenizeTimeLimit: 20
229
+ }),
230
+ count: document.blocks.reduce((count, block) => count + block.split(/\r\n|\n/).length, 0),
231
+ done: -1
232
+ };
233
+ count += entry.count;
234
+ entries.set(message.generation, entry);
235
+ }
236
+ return new Promise((resolve) => {
237
+ const id = key(message.generation, message.index);
238
+ const existing = requests.get(id);
239
+ const request = existing ?? { generation: message.generation, index: message.index, resolve: [] };
240
+ request.resolve.push(resolve);
241
+ requests.delete(id);
242
+ requests.set(id, request);
243
+ void pump();
244
+ });
245
+ }
246
+ };
247
+ }
248
+ export function serveDiffSyntax(endpoint) {
249
+ let highlighter;
250
+ const service = createSyntaxService({
251
+ highlighter: () => (highlighter ??= import('shiki').then(({ createHighlighter }) => createHighlighter({ langs: [], themes: [] }))),
252
+ send: (message, transfer) => endpoint.postMessage(message, { transfer: transfer ?? [] })
253
+ });
254
+ const listener = (event) => {
255
+ void service.handle(event.data).catch((error) => {
256
+ endpoint.postMessage({ type: 'error', message: error instanceof Error ? error.message : String(error) });
257
+ });
258
+ };
259
+ endpoint.addEventListener('message', listener);
260
+ return () => {
261
+ endpoint.removeEventListener('message', listener);
262
+ service.dispose();
263
+ void highlighter?.then((loaded) => loaded.dispose(), () => undefined);
264
+ };
265
+ }
package/dist/shiki.d.ts CHANGED
@@ -5,3 +5,4 @@ export { documentLanguageOf };
5
5
  export declare function loadGrammar(highlighter: Highlighter, grammar: string, text?: string): Promise<boolean>;
6
6
  export declare function shikiTokenizers(highlighter: () => Promise<Highlighter>): TokenizerSource;
7
7
  export declare function shikiScopeColors(highlighter: () => Promise<Highlighter>): ScopeColorSource;
8
+ export { shikiDocumentHighlighter, shikiDocumentTokens, type ShikiDocumentTokenOptions } from './shiki-document.ts';
package/dist/shiki.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import { documentLanguageOf, loadDocumentLanguage } from './php-html.js';
2
2
  import { scopeColorsOf } from './theme-scopes.js';
3
3
  export { documentLanguageOf };
4
+ /* The standard token types of TextMate: 1 comment, 2 string, 3 regex. */
5
+ const KINDS = { 1: 'comment', 2: 'string', 3: 'regex' };
4
6
  const PLAIN_TEXT = new Set(['', 'text', 'txt', 'plain', 'plaintext']);
5
7
  /* 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. */
6
- function lineTokenizer(highlighter, language, theme) {
8
+ function lineTokenizer(highlighter, language, theme, embedded) {
7
9
  let grammar = highlighter.getLanguage(language);
8
10
  return {
11
+ prepare: embedded.matcher === null ? undefined : (text) => loadEmbedded(highlighter, embedded, text),
9
12
  stale() {
10
13
  const current = highlighter.getLanguage(language);
11
14
  const changed = current !== grammar;
@@ -13,12 +16,18 @@ function lineTokenizer(highlighter, language, theme) {
13
16
  return changed;
14
17
  },
15
18
  tokenizeLine(text, state) {
16
- const options = { lang: language, theme, grammarState: (state ?? undefined) };
19
+ const options = {
20
+ lang: language,
21
+ theme,
22
+ grammarState: (state ?? undefined),
23
+ includeExplanation: 'tokenType'
24
+ };
17
25
  const lines = highlighter.codeToTokensBase(text, options);
18
26
  const tokens = (lines[0] ?? []).map((token) => ({
19
27
  length: token.content.length,
20
28
  color: token.color ?? '',
21
- fontStyle: token.fontStyle ?? 0
29
+ fontStyle: token.fontStyle ?? 0,
30
+ kind: KINDS[token.type ?? 0]
22
31
  }));
23
32
  return { tokens, state: highlighter.getLastGrammarState(lines) };
24
33
  },
@@ -32,39 +41,92 @@ function lineTokenizer(highlighter, language, theme) {
32
41
  }
33
42
  };
34
43
  }
35
- /* The words of a document that name a language, an alias or the id itself, and not part of a longer word. */
36
- function mentions(text, names) {
37
- return names.some((name) => new RegExp(`(?<![\\w-])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-])`, 'i').test(text));
38
- }
39
- /*
40
- * A grammar embeds some languages only on demand (Vue's `lang="scss"` styles, the fences of Markdown),
41
- * and Shiki builds the host grammar anew each time one of them loads later, which drops the states
42
- * of every line colored before. So the ones a document names are loaded together with the host.
43
- */
44
- async function loadEmbedded(highlighter, grammar, text) {
45
- const { bundledLanguages, bundledLanguagesInfo } = await import('shiki');
46
- const bundled = bundledLanguages[grammar];
47
- if (bundled === undefined) {
48
- return;
44
+ const languageLoads = new WeakMap();
45
+ const embeddedIndexes = new Map();
46
+ /* Concurrent documents await one load, so each learns the grammar was rebuilt. */
47
+ function loadOnce(highlighter, id) {
48
+ if (highlighter.getLoadedLanguages().includes(id)) {
49
+ return Promise.resolve();
49
50
  }
50
- const registrations = (await bundled()).default;
51
- const lazy = registrations.find((registration) => registration.name === grammar)?.embeddedLangsLazy ?? [];
52
- const named = lazy.filter((id) => {
53
- const aliases = bundledLanguagesInfo.find((info) => info.id === id)?.aliases ?? [];
54
- return !highlighter.getLoadedLanguages().includes(id) && mentions(text, [id, ...aliases]);
51
+ let loads = languageLoads.get(highlighter);
52
+ if (loads === undefined) {
53
+ loads = new Map();
54
+ languageLoads.set(highlighter, loads);
55
+ }
56
+ const pending = loads.get(id);
57
+ if (pending !== undefined) {
58
+ return pending;
59
+ }
60
+ const load = (async () => {
61
+ await loadDocumentLanguage(highlighter, id);
62
+ if (!highlighter.getLoadedLanguages().includes(id)) {
63
+ await highlighter.loadLanguage(id);
64
+ }
65
+ })();
66
+ loads.set(id, load);
67
+ void load.catch(() => {
68
+ if (loads.get(id) === load) {
69
+ loads.delete(id);
70
+ }
55
71
  });
56
- await Promise.allSettled(named.map((id) => highlighter.loadLanguage(id)));
72
+ return load;
57
73
  }
58
- /*
59
- * Loads the grammar of a language with what a document in it embeds, and says whether Shiki has it.
60
- * `text` is the document, which decides the embedded languages that load only when named.
61
- */
62
- export async function loadGrammar(highlighter, grammar, text = '') {
63
- await loadDocumentLanguage(highlighter, grammar);
64
- if (!highlighter.getLoadedLanguages().includes(grammar)) {
65
- await highlighter.loadLanguage(grammar);
74
+ function embedIndex(grammar) {
75
+ const cached = embeddedIndexes.get(grammar);
76
+ if (cached !== undefined) {
77
+ return cached;
78
+ }
79
+ const pending = (async () => {
80
+ const { bundledLanguages, bundledLanguagesInfo } = await import('shiki');
81
+ const bundled = bundledLanguages[grammar];
82
+ const registrations = bundled === undefined ? [] : (await bundled()).default;
83
+ const lazy = registrations.find((registration) => registration.name === grammar)?.embeddedLangsLazy ?? [];
84
+ const names = new Map();
85
+ for (const id of lazy) {
86
+ const aliases = bundledLanguagesInfo.find((info) => info.id === id)?.aliases ?? [];
87
+ for (const name of [id, ...aliases]) {
88
+ names.set(name.toLowerCase(), id);
89
+ }
90
+ }
91
+ if (names.size === 0) {
92
+ return { names, matcher: null };
93
+ }
94
+ let matcher;
95
+ if (grammar === 'markdown' || grammar === 'mdx') {
96
+ matcher = /^\s*(?:`{3,}|~{3,})[ \t]*([\w#+.-]+)/gm;
97
+ }
98
+ else if (grammar === 'vue' || grammar === 'svelte' || grammar === 'astro') {
99
+ matcher = /\blang\s*=\s*["']([\w#+.-]+)["']/gi;
100
+ }
101
+ else {
102
+ const alternatives = [...names.keys()].map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
103
+ matcher = new RegExp(`(?<![\\w-])(${alternatives})(?![\\w-])`, 'gi');
104
+ }
105
+ return { names, matcher };
106
+ })();
107
+ embeddedIndexes.set(grammar, pending);
108
+ void pending.catch(() => embeddedIndexes.delete(grammar));
109
+ return pending;
110
+ }
111
+ async function loadEmbedded(highlighter, embedded, text) {
112
+ if (embedded.matcher === null) {
113
+ return false;
114
+ }
115
+ const loaded = new Set(highlighter.getLoadedLanguages());
116
+ const named = new Set();
117
+ for (const match of text.matchAll(embedded.matcher)) {
118
+ const id = embedded.names.get(match[1].toLowerCase());
119
+ if (id !== undefined && !loaded.has(id)) {
120
+ named.add(id);
121
+ }
66
122
  }
67
- await loadEmbedded(highlighter, grammar, text);
123
+ const results = await Promise.allSettled([...named].map((id) => loadOnce(highlighter, id)));
124
+ return results.some((result) => result.status === 'fulfilled');
125
+ }
126
+ /* Text names the optional embedded grammars that must load before the host hands out states. */
127
+ export async function loadGrammar(highlighter, grammar, text = '') {
128
+ await loadOnce(highlighter, grammar);
129
+ await loadEmbedded(highlighter, await embedIndex(grammar), text);
68
130
  return highlighter.getLoadedLanguages().includes(grammar);
69
131
  }
70
132
  /*
@@ -82,7 +144,7 @@ export function shikiTokenizers(highlighter) {
82
144
  if (!shiki.getLoadedThemes().includes(theme)) {
83
145
  await shiki.loadTheme(theme);
84
146
  }
85
- return (await loadGrammar(shiki, grammar, text)) ? lineTokenizer(shiki, grammar, theme) : null;
147
+ return (await loadGrammar(shiki, grammar, text)) ? lineTokenizer(shiki, grammar, theme, await embedIndex(grammar)) : null;
86
148
  }
87
149
  catch {
88
150
  return null;
@@ -104,3 +166,4 @@ export function shikiScopeColors(highlighter) {
104
166
  }
105
167
  };
106
168
  }
169
+ export { shikiDocumentHighlighter, shikiDocumentTokens } from './shiki-document.js';
package/dist/testing.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface KeyOptions {
9
9
  metaKey?: boolean;
10
10
  altKey?: boolean;
11
11
  shiftKey?: boolean;
12
+ pointerType?: string;
12
13
  }
13
14
  export declare function press(window: Window, target: Element, key: string, options?: KeyOptions): boolean;
14
15
  export declare function release(window: Window, target: Element, key: string, options?: KeyOptions): void;
package/dist/tokens.d.ts CHANGED
@@ -8,6 +8,7 @@ export declare class TokenCache {
8
8
  private tokenizer;
9
9
  private entries;
10
10
  private frontier;
11
+ private staleFrom;
11
12
  private reusableFrom;
12
13
  private pivotLine;
13
14
  private pivotState;
@@ -20,6 +21,7 @@ export declare class TokenCache {
20
21
  tokenizeText(lines: readonly string[]): (readonly LineToken[])[] | null;
21
22
  tokensOf(line: number): readonly LineToken[] | null;
22
23
  edited(batches: readonly (readonly DocumentChange[])[], lineOf: (offset: number) => number): void;
24
+ revalidate(): boolean;
23
25
  advance(until: number, budget: number, now?: () => number): {
24
26
  from: number;
25
27
  to: number;
package/dist/tokens.js CHANGED
@@ -12,6 +12,8 @@ export class TokenCache {
12
12
  entries = [];
13
13
  /* Every line above it is colored for the text as it is now. */
14
14
  frontier = 0;
15
+ /* Colors survive a rebuild, but the states below this boundary cannot settle recoloring. */
16
+ staleFrom = Number.POSITIVE_INFINITY;
15
17
  /* The last line the edit touched; lines below it may be told right by their old state. */
16
18
  reusableFrom = Number.POSITIVE_INFINITY;
17
19
  /* The state the last line of the edited stretch ended in before the edit; the same state now means the lines below are right. */
@@ -31,6 +33,7 @@ export class TokenCache {
31
33
  this.tokenizer = tokenizer;
32
34
  this.lines = this.source.getLineCount();
33
35
  this.entries = [];
36
+ this.staleFrom = Number.POSITIVE_INFINITY;
34
37
  this.frontier = 0;
35
38
  this.forgetPivot();
36
39
  }
@@ -69,6 +72,7 @@ export class TokenCache {
69
72
  const first = batches[0]?.[0];
70
73
  if (first === undefined) {
71
74
  this.entries = [];
75
+ this.staleFrom = Number.POSITIVE_INFINITY;
72
76
  this.frontier = 0;
73
77
  this.forgetPivot();
74
78
  return;
@@ -83,8 +87,22 @@ export class TokenCache {
83
87
  const lastNew = last === undefined ? -1 : lineOf(last.from + shift + last.insertedLength);
84
88
  const lastOld = lastNew - delta;
85
89
  this.frontier = Math.min(this.frontier, firstLine);
90
+ if (last === undefined && Number.isFinite(this.staleFrom)) {
91
+ // A history batch has no single mapping for the old grammar boundary.
92
+ this.staleFrom = 0;
93
+ this.frontier = 0;
94
+ }
95
+ else if (this.staleFrom > lastOld) {
96
+ this.staleFrom += delta;
97
+ }
98
+ else if (this.staleFrom > firstLine) {
99
+ this.staleFrom = firstLine;
100
+ }
86
101
  if (last === undefined || lastOld < firstLine || this.entries.length <= firstLine) {
87
102
  this.entries.length = Math.min(this.entries.length, firstLine);
103
+ if (this.staleFrom >= this.entries.length) {
104
+ this.staleFrom = Number.POSITIVE_INFINITY;
105
+ }
88
106
  this.forgetPivot();
89
107
  return;
90
108
  }
@@ -94,6 +112,16 @@ export class TokenCache {
94
112
  this.entries.splice(firstLine, lastOld - firstLine + 1, ...new Array(lastNew - firstLine + 1));
95
113
  this.reusableFrom = lastNew + 1;
96
114
  }
115
+ /* Old colors stay visible until the rebuilt grammar reaches each line. */
116
+ revalidate() {
117
+ if (this.tokenizer?.stale?.() !== true) {
118
+ return false;
119
+ }
120
+ this.frontier = 0;
121
+ this.staleFrom = 0;
122
+ this.forgetPivot();
123
+ return true;
124
+ }
97
125
  /*
98
126
  * Colors lines from the frontier until `until` is reached, the colors are right for the rest of
99
127
  * the document, or `budget` milliseconds are spent. Returns the first line that changed, so a
@@ -104,10 +132,7 @@ export class TokenCache {
104
132
  if (tokenizer === null) {
105
133
  return null;
106
134
  }
107
- if (tokenizer.stale?.() === true) {
108
- this.frontier = 0;
109
- this.forgetPivot();
110
- }
135
+ this.revalidate();
111
136
  const count = this.source.getLineCount();
112
137
  const limit = Math.min(until, count - 1);
113
138
  const deadline = now() + budget;
@@ -127,10 +152,15 @@ export class TokenCache {
127
152
  }
128
153
  this.entries[line] = next;
129
154
  this.frontier = line + 1;
130
- const settled = (line === this.pivotLine && tokenizer.sameState(this.pivotState, next.state)) ||
131
- (previous !== undefined && line >= this.reusableFrom && tokenizer.sameState(previous.state, next.state));
155
+ const fresh = line < this.staleFrom;
156
+ if (line === this.staleFrom) {
157
+ this.staleFrom = line + 1 >= this.entries.length ? Number.POSITIVE_INFINITY : line + 1;
158
+ }
159
+ const settled = fresh &&
160
+ ((line === this.pivotLine && tokenizer.sameState(this.pivotState, next.state)) ||
161
+ (previous !== undefined && line >= this.reusableFrom && tokenizer.sameState(previous.state, next.state)));
132
162
  if (settled) {
133
- this.frontier = this.entries.length;
163
+ this.frontier = Math.min(this.entries.length, this.staleFrom);
134
164
  this.forgetPivot();
135
165
  break;
136
166
  }
@@ -0,0 +1,27 @@
1
+ import type { DocumentModel } from '@adecore/editor-core';
2
+ export interface TouchHost {
3
+ readonly model: DocumentModel;
4
+ offsetAt(clientX: number, clientY: number): number;
5
+ camelHumps(): boolean;
6
+ lineAt(offset: number): {
7
+ from: number;
8
+ to: number;
9
+ };
10
+ focus(): void;
11
+ }
12
+ export declare class TouchSelection {
13
+ private contact;
14
+ private timer;
15
+ private lastTap;
16
+ private readonly host;
17
+ constructor(host: TouchHost);
18
+ get pressed(): boolean;
19
+ start(event: PointerEvent, area?: 'text' | 'gutter', taken?: () => boolean): void;
20
+ move(event: PointerEvent): void;
21
+ end(event: PointerEvent): void;
22
+ dispose(): void;
23
+ private tap;
24
+ private press;
25
+ private clearTimer;
26
+ private cancel;
27
+ }