@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.
- package/dist/attribution.d.ts +9 -8
- package/dist/attribution.js +44 -9
- package/dist/bidi.d.ts +44 -0
- package/dist/bidi.js +26 -0
- package/dist/column-selection.js +2 -1
- package/dist/controller.d.ts +6 -1
- package/dist/controller.js +93 -3
- package/dist/editor.css +81 -1
- package/dist/engine.js +184 -41
- package/dist/fake.d.ts +43 -5
- package/dist/fake.js +117 -13
- package/dist/find-search.d.ts +26 -0
- package/dist/find-search.js +130 -0
- package/dist/find.d.ts +50 -2
- package/dist/find.js +575 -25
- package/dist/geometry-stops.d.ts +6 -0
- package/dist/geometry-stops.js +47 -0
- package/dist/grammar-preparation.d.ts +29 -0
- package/dist/grammar-preparation.js +147 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/keymap-table.d.ts +10 -1
- package/dist/keymap-table.js +37 -1
- package/dist/keymap.d.ts +1 -0
- package/dist/keymap.js +4 -1
- package/dist/layout-changes.d.ts +6 -0
- package/dist/layout-changes.js +27 -0
- package/dist/layout.d.ts +45 -2
- package/dist/layout.js +202 -58
- package/dist/offsets.d.ts +1 -0
- package/dist/offsets.js +23 -0
- package/dist/outline.js +5 -4
- package/dist/paint.d.ts +11 -3
- package/dist/paint.js +180 -34
- package/dist/search-client.d.ts +27 -0
- package/dist/search-client.js +210 -0
- package/dist/search-protocol.d.ts +103 -0
- package/dist/search-protocol.js +3 -0
- package/dist/search-worker.d.ts +11 -0
- package/dist/search-worker.js +137 -0
- package/dist/semantic.d.ts +2 -1
- package/dist/semantic.js +6 -1
- package/dist/shiki-document.d.ts +8 -0
- package/dist/shiki-document.js +93 -0
- package/dist/shiki-worker.d.ts +65 -0
- package/dist/shiki-worker.js +265 -0
- package/dist/shiki.d.ts +1 -0
- package/dist/shiki.js +96 -33
- package/dist/testing.d.ts +1 -0
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.js +37 -7
- package/dist/touch.d.ts +27 -0
- package/dist/touch.js +100 -0
- package/dist/types.d.ts +84 -2
- package/dist/uniform-geometry.d.ts +7 -0
- package/dist/uniform-geometry.js +49 -0
- package/dist/view.d.ts +68 -17
- package/dist/view.js +424 -97
- package/dist/worker-client.d.ts +37 -0
- package/dist/worker-client.js +195 -0
- package/package.json +17 -2
- package/src/attribution.ts +50 -14
- package/src/bidi.ts +32 -0
- package/src/column-selection.ts +2 -1
- package/src/controller.ts +97 -3
- package/src/editor.css +81 -1
- package/src/engine.ts +216 -42
- package/src/fake.ts +152 -14
- package/src/find-search.ts +165 -0
- package/src/find.ts +622 -21
- package/src/geometry-stops.ts +51 -0
- package/src/grammar-preparation.ts +158 -0
- package/src/index.ts +10 -0
- package/src/keymap-table.ts +44 -2
- package/src/keymap.ts +5 -1
- package/src/layout-changes.ts +33 -0
- package/src/layout.ts +262 -63
- package/src/offsets.ts +24 -0
- package/src/outline.ts +5 -4
- package/src/paint.ts +202 -36
- package/src/search-client.ts +306 -0
- package/src/search-protocol.ts +100 -0
- package/src/search-worker.ts +171 -0
- package/src/semantic.ts +8 -2
- package/src/shiki-document.ts +143 -0
- package/src/shiki-worker.ts +344 -0
- package/src/shiki.ts +103 -33
- package/src/testing.ts +2 -0
- package/src/tokens.ts +36 -7
- package/src/touch.ts +134 -0
- package/src/types.ts +174 -8
- package/src/uniform-geometry.ts +57 -0
- package/src/view.ts +500 -102
- package/src/worker-client.ts +212 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { FindMatch } from '@adecore/editor-core';
|
|
2
|
+
import type { EditorFindQuery } from './types.ts';
|
|
3
|
+
export declare const SEARCH_LIMIT = 10000;
|
|
4
|
+
export declare const MAX_REPLACEMENT_PAGE = 10000;
|
|
5
|
+
export declare const MAX_REPLACEMENT_CHARACTERS = 4000000;
|
|
6
|
+
export interface EditorSearchSnapshot {
|
|
7
|
+
readonly revision: number;
|
|
8
|
+
readonly text: string;
|
|
9
|
+
}
|
|
10
|
+
export interface EditorSearchOptions {
|
|
11
|
+
readonly deadline?: number;
|
|
12
|
+
readonly from?: number;
|
|
13
|
+
readonly to?: number;
|
|
14
|
+
readonly maxResults?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface EditorSearchResult {
|
|
17
|
+
readonly revision: number;
|
|
18
|
+
readonly ranges: Uint32Array;
|
|
19
|
+
readonly limited: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface EditorSearchRange {
|
|
22
|
+
readonly from: number;
|
|
23
|
+
readonly to: number;
|
|
24
|
+
}
|
|
25
|
+
export interface EditorSearchNextOptions {
|
|
26
|
+
readonly deadline?: number;
|
|
27
|
+
readonly from?: number;
|
|
28
|
+
readonly to?: number;
|
|
29
|
+
readonly backwards?: boolean;
|
|
30
|
+
readonly wrap?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface EditorSearchReplacementOptions {
|
|
33
|
+
readonly deadline?: number;
|
|
34
|
+
readonly from?: number;
|
|
35
|
+
readonly to?: number;
|
|
36
|
+
readonly preserveCase?: boolean;
|
|
37
|
+
readonly maxEdits?: number;
|
|
38
|
+
readonly maxCharacters?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface EditorSearchReplacements {
|
|
41
|
+
readonly revision: number;
|
|
42
|
+
readonly ranges: Uint32Array;
|
|
43
|
+
readonly text: string;
|
|
44
|
+
readonly next: number | null;
|
|
45
|
+
}
|
|
46
|
+
export type EditorSearchMatch = Omit<FindMatch, 'revision' | 'text'>;
|
|
47
|
+
interface SearchInput {
|
|
48
|
+
readonly id: number;
|
|
49
|
+
readonly revision: number;
|
|
50
|
+
readonly text?: string;
|
|
51
|
+
readonly query: EditorFindQuery;
|
|
52
|
+
}
|
|
53
|
+
export type EditorSearchInput = (SearchInput & {
|
|
54
|
+
readonly type: 'search';
|
|
55
|
+
readonly from?: number;
|
|
56
|
+
readonly to?: number;
|
|
57
|
+
readonly maxResults: number;
|
|
58
|
+
}) | (SearchInput & {
|
|
59
|
+
readonly type: 'match';
|
|
60
|
+
readonly from: number;
|
|
61
|
+
readonly to: number;
|
|
62
|
+
}) | (SearchInput & {
|
|
63
|
+
readonly type: 'next';
|
|
64
|
+
readonly offset: number;
|
|
65
|
+
readonly from?: number;
|
|
66
|
+
readonly to?: number;
|
|
67
|
+
readonly backwards?: boolean;
|
|
68
|
+
readonly wrap?: boolean;
|
|
69
|
+
}) | (SearchInput & {
|
|
70
|
+
readonly type: 'replacements';
|
|
71
|
+
readonly from?: number;
|
|
72
|
+
readonly to?: number;
|
|
73
|
+
readonly replacement: string;
|
|
74
|
+
readonly preserveCase?: boolean;
|
|
75
|
+
readonly maxEdits: number;
|
|
76
|
+
readonly maxCharacters: number;
|
|
77
|
+
});
|
|
78
|
+
export type EditorSearchOutput = {
|
|
79
|
+
readonly type: 'ready';
|
|
80
|
+
} | ({
|
|
81
|
+
readonly type: 'matches';
|
|
82
|
+
readonly id: number;
|
|
83
|
+
} & EditorSearchResult) | {
|
|
84
|
+
readonly type: 'match';
|
|
85
|
+
readonly id: number;
|
|
86
|
+
readonly revision: number;
|
|
87
|
+
readonly match: EditorSearchMatch | null;
|
|
88
|
+
} | {
|
|
89
|
+
readonly type: 'next';
|
|
90
|
+
readonly id: number;
|
|
91
|
+
readonly revision: number;
|
|
92
|
+
readonly range: EditorSearchRange | null;
|
|
93
|
+
} | ({
|
|
94
|
+
readonly type: 'replacements';
|
|
95
|
+
readonly id: number;
|
|
96
|
+
} & EditorSearchReplacements) | {
|
|
97
|
+
readonly type: 'error';
|
|
98
|
+
readonly id: number;
|
|
99
|
+
readonly revision: number;
|
|
100
|
+
readonly kind: 'syntax' | 'range' | 'snapshot' | 'error';
|
|
101
|
+
readonly message: string;
|
|
102
|
+
};
|
|
103
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type EditorSearchInput, type EditorSearchOutput } from './search-protocol.ts';
|
|
2
|
+
export { createSearchClient, EditorSearchError, type EditorSearchClient, type EditorSearchClientOptions, type EditorSearchErrorKind } from './search-client.ts';
|
|
3
|
+
export type { EditorSearchInput, EditorSearchOptions, EditorSearchOutput, EditorSearchResult, EditorSearchSnapshot, EditorSearchMatch, EditorSearchRange, EditorSearchNextOptions, EditorSearchReplacementOptions, EditorSearchReplacements } from './search-protocol.ts';
|
|
4
|
+
export interface EditorSearchWorkerEndpoint {
|
|
5
|
+
addEventListener(type: 'message', listener: (event: MessageEvent<EditorSearchInput>) => void): void;
|
|
6
|
+
removeEventListener(type: 'message', listener: (event: MessageEvent<EditorSearchInput>) => void): void;
|
|
7
|
+
postMessage(message: EditorSearchOutput, options?: {
|
|
8
|
+
transfer: Transferable[];
|
|
9
|
+
}): void;
|
|
10
|
+
}
|
|
11
|
+
export declare function serveEditorSearch(endpoint: EditorSearchWorkerEndpoint): () => void;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { iterateMatches, findMatchFrom, replacementText } from '@adecore/editor-core';
|
|
2
|
+
import { SEARCH_LIMIT, MAX_REPLACEMENT_PAGE, MAX_REPLACEMENT_CHARACTERS } from './search-protocol.js';
|
|
3
|
+
export { createSearchClient, EditorSearchError } from './search-client.js';
|
|
4
|
+
const MAX_CAPTURE_CHARACTERS = 4_000_000;
|
|
5
|
+
function findOptions(request) {
|
|
6
|
+
return { caseSensitive: request.query.caseSensitive, wholeWord: request.query.wholeWord, regex: request.query.regex, from: request.from, to: request.to };
|
|
7
|
+
}
|
|
8
|
+
function captureMatch(found) {
|
|
9
|
+
let characters = 0;
|
|
10
|
+
for (const value of found.captures) {
|
|
11
|
+
characters += value?.length ?? 0;
|
|
12
|
+
}
|
|
13
|
+
for (const value of Object.values(found.groups ?? {})) {
|
|
14
|
+
characters += value?.length ?? 0;
|
|
15
|
+
}
|
|
16
|
+
if (characters > MAX_CAPTURE_CHARACTERS) {
|
|
17
|
+
throw new RangeError('Search captures exceed the response size limit.');
|
|
18
|
+
}
|
|
19
|
+
return { from: found.from, to: found.to, captures: found.captures, groups: found.groups, regex: found.regex };
|
|
20
|
+
}
|
|
21
|
+
function searchReply(request, snapshot) {
|
|
22
|
+
const { text } = snapshot;
|
|
23
|
+
const options = findOptions(request);
|
|
24
|
+
const header = { id: request.id, revision: request.revision };
|
|
25
|
+
if (request.type === 'next') {
|
|
26
|
+
let found = null;
|
|
27
|
+
if (request.backwards) {
|
|
28
|
+
let last = null;
|
|
29
|
+
for (const match of iterateMatches(text, request.query.text, request.revision, options)) {
|
|
30
|
+
last = match;
|
|
31
|
+
if (match.from < request.offset) {
|
|
32
|
+
found = match;
|
|
33
|
+
}
|
|
34
|
+
else if (found !== null || request.wrap === false) {
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
found ??= request.wrap === false ? null : last;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
found = findMatchFrom(text, request.query.text, request.revision, request.offset, { ...options, wrap: request.wrap });
|
|
42
|
+
}
|
|
43
|
+
return { type: 'next', ...header, range: found === null ? null : { from: found.from, to: found.to } };
|
|
44
|
+
}
|
|
45
|
+
if (request.type === 'match') {
|
|
46
|
+
const found = iterateMatches(text, request.query.text, request.revision, { ...options, maxResults: 1 }).next().value;
|
|
47
|
+
return { type: 'match', ...header, match: found && found.from === request.from && found.to === request.to ? captureMatch(found) : null };
|
|
48
|
+
}
|
|
49
|
+
if (request.type === 'replacements') {
|
|
50
|
+
if (!Number.isInteger(request.maxEdits) ||
|
|
51
|
+
request.maxEdits < 1 ||
|
|
52
|
+
request.maxEdits > MAX_REPLACEMENT_PAGE ||
|
|
53
|
+
!Number.isInteger(request.maxCharacters) ||
|
|
54
|
+
request.maxCharacters < 0 ||
|
|
55
|
+
request.maxCharacters > MAX_REPLACEMENT_CHARACTERS) {
|
|
56
|
+
throw new RangeError('Replacement pages need a positive edit limit and a bounded character limit.');
|
|
57
|
+
}
|
|
58
|
+
const ranges = [], pieces = [];
|
|
59
|
+
let characters = 0;
|
|
60
|
+
let resume = request.from ?? 0;
|
|
61
|
+
let next = null;
|
|
62
|
+
for (const match of iterateMatches(text, request.query.text, request.revision, { ...options, maxResults: request.maxEdits + 1 })) {
|
|
63
|
+
if (pieces.length === request.maxEdits) {
|
|
64
|
+
next = resume;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
const replacement = replacementText(text, match, request.replacement, false, request.preserveCase, request.maxCharacters);
|
|
68
|
+
if (characters + replacement.length > request.maxCharacters) {
|
|
69
|
+
if (pieces.length === 0) {
|
|
70
|
+
throw new RangeError('One replacement exceeds the response size limit.');
|
|
71
|
+
}
|
|
72
|
+
next = resume;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
characters += replacement.length;
|
|
76
|
+
ranges.push(match.from, match.to, replacement.length);
|
|
77
|
+
pieces.push(replacement);
|
|
78
|
+
// Resuming after an empty match must skip the same full code point as the matcher.
|
|
79
|
+
resume = match.to + (match.from === match.to ? ((text.codePointAt(match.to) ?? 0) > 0xffff ? 2 : 1) : 0);
|
|
80
|
+
}
|
|
81
|
+
return { type: 'replacements', ...header, ranges: Uint32Array.from(ranges), text: pieces.join(''), next };
|
|
82
|
+
}
|
|
83
|
+
if (!Number.isInteger(request.maxResults) || request.maxResults < 0 || request.maxResults > SEARCH_LIMIT) {
|
|
84
|
+
throw new RangeError(`Search results must be limited to ${SEARCH_LIMIT} matches or fewer.`);
|
|
85
|
+
}
|
|
86
|
+
const offsets = [];
|
|
87
|
+
let limited = false;
|
|
88
|
+
for (const match of iterateMatches(text, request.query.text, request.revision, { ...options, maxResults: request.maxResults + 1 })) {
|
|
89
|
+
if (offsets.length === request.maxResults * 2) {
|
|
90
|
+
limited = true;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
offsets.push(match.from, match.to);
|
|
94
|
+
}
|
|
95
|
+
return { type: 'matches', ...header, ranges: Uint32Array.from(offsets), limited };
|
|
96
|
+
}
|
|
97
|
+
/* A running RegExp can only be interrupted by terminating its worker. */
|
|
98
|
+
export function serveEditorSearch(endpoint) {
|
|
99
|
+
let snapshot;
|
|
100
|
+
const listener = ({ data: request }) => {
|
|
101
|
+
try {
|
|
102
|
+
if (!Number.isSafeInteger(request.revision) || request.revision < 0) {
|
|
103
|
+
throw new RangeError('The search revision must be a nonnegative safe integer.');
|
|
104
|
+
}
|
|
105
|
+
if (request.text !== undefined) {
|
|
106
|
+
snapshot = { revision: request.revision, text: request.text };
|
|
107
|
+
}
|
|
108
|
+
if (snapshot === undefined || snapshot.revision !== request.revision) {
|
|
109
|
+
endpoint.postMessage({
|
|
110
|
+
type: 'error',
|
|
111
|
+
id: request.id,
|
|
112
|
+
revision: request.revision,
|
|
113
|
+
kind: 'snapshot',
|
|
114
|
+
message: 'The search worker needs the text of this revision.'
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const response = searchReply(request, snapshot);
|
|
119
|
+
endpoint.postMessage(response, response.type === 'matches' || response.type === 'replacements' ? { transfer: [response.ranges.buffer] } : undefined);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
endpoint.postMessage({
|
|
123
|
+
type: 'error',
|
|
124
|
+
id: request.id,
|
|
125
|
+
revision: request.revision,
|
|
126
|
+
kind: error instanceof SyntaxError ? 'syntax' : error instanceof RangeError ? 'range' : 'error',
|
|
127
|
+
message: error instanceof Error ? error.message : String(error)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
endpoint.addEventListener('message', listener);
|
|
132
|
+
endpoint.postMessage({ type: 'ready' });
|
|
133
|
+
return () => {
|
|
134
|
+
endpoint.removeEventListener('message', listener);
|
|
135
|
+
snapshot = undefined;
|
|
136
|
+
};
|
|
137
|
+
}
|
package/dist/semantic.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { LineToken } from './types.ts';
|
|
1
|
+
import type { LineToken, LineTokenKind } from './types.ts';
|
|
2
2
|
export interface SemanticSpan {
|
|
3
3
|
readonly from: number;
|
|
4
4
|
readonly to: number;
|
|
5
5
|
readonly color: string;
|
|
6
6
|
readonly fontStyle: number;
|
|
7
|
+
readonly within?: LineTokenKind;
|
|
7
8
|
}
|
|
8
9
|
export declare function overlayTokens(base: readonly LineToken[], spans: readonly SemanticSpan[]): LineToken[];
|
package/dist/semantic.js
CHANGED
|
@@ -37,7 +37,12 @@ export function overlayTokens(base, spans) {
|
|
|
37
37
|
}
|
|
38
38
|
else {
|
|
39
39
|
const stop = Math.min(end, current.to);
|
|
40
|
-
|
|
40
|
+
if (current.within === undefined || token.kind === current.within) {
|
|
41
|
+
push(stop - at, current.color, current.fontStyle);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
push(stop - at, token.color, token.fontStyle);
|
|
45
|
+
}
|
|
41
46
|
at = stop;
|
|
42
47
|
}
|
|
43
48
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Highlighter, ShikiTransformer, ThemedToken } from 'shiki';
|
|
2
|
+
export interface ShikiDocumentTokenOptions {
|
|
3
|
+
readonly reset?: ReadonlySet<number>;
|
|
4
|
+
readonly tokenizeMaxLineLength?: number;
|
|
5
|
+
readonly tokenizeTimeLimit?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function shikiDocumentHighlighter(highlighter: () => Promise<Highlighter>, blocks: readonly string[], language: string, theme: string, transformers?: readonly ShikiTransformer[]): (index: number) => Promise<string>;
|
|
8
|
+
export declare function shikiDocumentTokens(highlighter: () => Promise<Highlighter>, blocks: readonly string[], language: string, theme: string, options?: ShikiDocumentTokenOptions): (index: number) => Promise<ThemedToken[][]>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { documentLanguageOf, loadGrammar } from './shiki.js';
|
|
2
|
+
export function shikiDocumentHighlighter(highlighter, blocks, language, theme, transformers = []) {
|
|
3
|
+
return documentHighlighter(highlighter, blocks, language, theme, (loaded, code, grammar, theme, state) => {
|
|
4
|
+
let root;
|
|
5
|
+
const value = loaded.codeToHtml(code, {
|
|
6
|
+
lang: grammar,
|
|
7
|
+
theme,
|
|
8
|
+
grammarState: state,
|
|
9
|
+
transformers: [
|
|
10
|
+
...transformers,
|
|
11
|
+
{
|
|
12
|
+
root(node) {
|
|
13
|
+
root = node;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
});
|
|
18
|
+
return { value, state: root === undefined ? undefined : loaded.getLastGrammarState(root) };
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function shikiDocumentTokens(highlighter, blocks, language, theme, options = {}) {
|
|
22
|
+
return documentHighlighter(highlighter, blocks, language, theme, (loaded, code, grammar, theme, state) => {
|
|
23
|
+
// Shiki 4.4 caches the active theme by argument identity, even after a same-name replacement.
|
|
24
|
+
const value = loaded.codeToTokensBase(code, {
|
|
25
|
+
lang: grammar,
|
|
26
|
+
theme: loaded.getTheme(theme),
|
|
27
|
+
grammarState: state,
|
|
28
|
+
tokenizeMaxLineLength: options.tokenizeMaxLineLength,
|
|
29
|
+
tokenizeTimeLimit: options.tokenizeTimeLimit
|
|
30
|
+
});
|
|
31
|
+
return { value, state: loaded.getLastGrammarState(value) };
|
|
32
|
+
}, options.reset);
|
|
33
|
+
}
|
|
34
|
+
function documentHighlighter(highlighter, blocks, language, theme, render, reset = new Set()) {
|
|
35
|
+
const resets = new Set(reset);
|
|
36
|
+
// A CR before the removed LF belongs to the block separator, not the last source line.
|
|
37
|
+
const code = blocks.map((block, index) => (index < blocks.length - 1 ? block.replace(/\r$/, '') : block));
|
|
38
|
+
let prepared;
|
|
39
|
+
let pending = Promise.resolve();
|
|
40
|
+
let grammar;
|
|
41
|
+
let themeRegistration;
|
|
42
|
+
let state;
|
|
43
|
+
let values = [];
|
|
44
|
+
const prepare = async () => {
|
|
45
|
+
const loaded = await highlighter();
|
|
46
|
+
if (!loaded.getLoadedThemes().includes(theme)) {
|
|
47
|
+
await loaded.loadTheme(theme);
|
|
48
|
+
}
|
|
49
|
+
const documentLanguage = documentLanguageOf(language);
|
|
50
|
+
const known = loaded.getLoadedLanguages().includes(documentLanguage) || documentLanguage in loaded.getBundledLanguages() || documentLanguage !== language;
|
|
51
|
+
const grammar = known && (await loadGrammar(loaded, documentLanguage, code.join('\n'))) ? documentLanguage : 'text';
|
|
52
|
+
return { highlighter: loaded, language: grammar };
|
|
53
|
+
};
|
|
54
|
+
return (index) => {
|
|
55
|
+
if (!Number.isInteger(index) || index < 0 || index >= code.length) {
|
|
56
|
+
return Promise.reject(new RangeError('The highlighted block index is outside the document.'));
|
|
57
|
+
}
|
|
58
|
+
const result = pending.then(async () => {
|
|
59
|
+
prepared ??= prepare().catch((error) => {
|
|
60
|
+
prepared = undefined;
|
|
61
|
+
throw error;
|
|
62
|
+
});
|
|
63
|
+
const { highlighter: loaded, language: grammarId } = await prepared;
|
|
64
|
+
const refresh = () => {
|
|
65
|
+
const currentGrammar = grammarId === 'text' ? undefined : loaded.getLanguage(grammarId);
|
|
66
|
+
const currentTheme = loaded.getTheme(theme);
|
|
67
|
+
// Loading an embedded language rebuilds Shiki's grammar and invalidates its old states.
|
|
68
|
+
if (grammar !== currentGrammar || themeRegistration !== currentTheme) {
|
|
69
|
+
grammar = currentGrammar;
|
|
70
|
+
themeRegistration = currentTheme;
|
|
71
|
+
state = undefined;
|
|
72
|
+
values = [];
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
refresh();
|
|
76
|
+
while (values.length <= index) {
|
|
77
|
+
const block = values.length;
|
|
78
|
+
const rendered = render(loaded, code[block], grammarId, theme, state);
|
|
79
|
+
state = resets.has(block + 1) ? undefined : rendered.state;
|
|
80
|
+
values.push(rendered.value);
|
|
81
|
+
if (block < index) {
|
|
82
|
+
// A jump deep into a file must leave room for input and painting between blocks.
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
84
|
+
refresh();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return values[index];
|
|
88
|
+
});
|
|
89
|
+
// A failed theme or grammar load can be retried without rejecting every later request.
|
|
90
|
+
pending = result.then(() => undefined, () => undefined);
|
|
91
|
+
return result;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Highlighter, ThemeRegistration } from 'shiki';
|
|
2
|
+
import type { LineToken } from './types.ts';
|
|
3
|
+
export interface DiffSyntaxDocument {
|
|
4
|
+
readonly language: string;
|
|
5
|
+
readonly theme: string;
|
|
6
|
+
readonly blocks: readonly string[];
|
|
7
|
+
readonly resets: readonly number[];
|
|
8
|
+
}
|
|
9
|
+
export type DiffSyntaxInput = {
|
|
10
|
+
readonly type: 'start';
|
|
11
|
+
readonly themes: readonly ThemeRegistration[];
|
|
12
|
+
readonly maxLines: number;
|
|
13
|
+
} | {
|
|
14
|
+
readonly type: 'request';
|
|
15
|
+
readonly generation: number;
|
|
16
|
+
readonly index: number;
|
|
17
|
+
readonly document?: DiffSyntaxDocument;
|
|
18
|
+
} | {
|
|
19
|
+
readonly type: 'cancel';
|
|
20
|
+
readonly generation: number;
|
|
21
|
+
} | {
|
|
22
|
+
readonly type: 'dispose';
|
|
23
|
+
readonly generation: number;
|
|
24
|
+
};
|
|
25
|
+
export interface DiffSyntaxTokens {
|
|
26
|
+
readonly type: 'tokens';
|
|
27
|
+
readonly generation: number;
|
|
28
|
+
readonly index: number;
|
|
29
|
+
readonly colors: readonly string[];
|
|
30
|
+
readonly foreground: string;
|
|
31
|
+
readonly background: string;
|
|
32
|
+
readonly lines: Uint32Array;
|
|
33
|
+
readonly tokens: Uint32Array;
|
|
34
|
+
}
|
|
35
|
+
export type DiffSyntaxOutput = DiffSyntaxTokens | {
|
|
36
|
+
readonly type: 'ready';
|
|
37
|
+
} | {
|
|
38
|
+
readonly type: 'error';
|
|
39
|
+
readonly message: string;
|
|
40
|
+
} | {
|
|
41
|
+
readonly type: 'failed';
|
|
42
|
+
readonly generation: number;
|
|
43
|
+
readonly index: number;
|
|
44
|
+
readonly reason: 'missing' | 'highlight';
|
|
45
|
+
readonly message: string;
|
|
46
|
+
};
|
|
47
|
+
export interface DiffSyntaxService {
|
|
48
|
+
handle(message: DiffSyntaxInput): Promise<void>;
|
|
49
|
+
dispose(): void;
|
|
50
|
+
}
|
|
51
|
+
export interface DiffSyntaxServiceOptions {
|
|
52
|
+
readonly highlighter: () => Promise<Highlighter>;
|
|
53
|
+
readonly send: (message: DiffSyntaxOutput, transfer?: Transferable[]) => void;
|
|
54
|
+
readonly yield?: () => Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
export declare function lineTokensOf(packet: DiffSyntaxTokens, line: number, text: string): LineToken[];
|
|
57
|
+
export declare function createSyntaxService(options: DiffSyntaxServiceOptions): DiffSyntaxService;
|
|
58
|
+
export interface DiffSyntaxWorkerEndpoint {
|
|
59
|
+
addEventListener(type: 'message', listener: (event: MessageEvent<DiffSyntaxInput>) => void): void;
|
|
60
|
+
removeEventListener(type: 'message', listener: (event: MessageEvent<DiffSyntaxInput>) => void): void;
|
|
61
|
+
postMessage(message: DiffSyntaxOutput, options?: {
|
|
62
|
+
transfer: Transferable[];
|
|
63
|
+
}): void;
|
|
64
|
+
}
|
|
65
|
+
export declare function serveDiffSyntax(endpoint: DiffSyntaxWorkerEndpoint): () => void;
|