@coldsmirk/inkstone-monaco 0.28.1 → 0.30.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/README.md +6 -5
- package/dist/bounds-CZ_iAUoc.js +120 -0
- package/dist/index.d.ts +25 -14
- package/dist/index.js +385 -52
- package/dist/workers/textmate.worker.js +296 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Part of [inkstone](https://github.com/coldsmirk/inkstone). For a drop-in React c
|
|
|
6
6
|
|
|
7
7
|
## What you get
|
|
8
8
|
|
|
9
|
-
- **`ensureMonacoHost(options?)`** — the process-wide host: resolves the local `monaco-editor` module (never the CDN), routes **all** built-in language workers (base editor, TypeScript/JavaScript, JSON, CSS/scss/less, HTML/handlebars/razor) as bundler-emitted chunks so everything works offline, and applies `locale: "zh-cn"` via Monaco's official NLS catalog when asked — plus, on every editor created under that host, zh-CN descriptions for the six command-palette actions Monaco describes in English literals that no catalog can reach (Fold, Unfold, Find with Arguments, Refactor..., Source Action..., Paste As...), each held to the installed `monaco-editor` by an upgrade-tripwire test. Idempotent — every caller gets the same module; a load failure is not cached, so a fresh call retries.
|
|
9
|
+
- **`ensureMonacoHost(options?)`** — the process-wide host: resolves the local `monaco-editor` module (never the CDN) and the parts of it the Shiki tokenization stands on, routes **all** built-in language workers (base editor, TypeScript/JavaScript, JSON, CSS/scss/less, HTML/handlebars/razor) and inkstone's own tokenization worker (the `textmate` label) as bundler-emitted chunks so everything works offline, and applies `locale: "zh-cn"` via Monaco's official NLS catalog when asked — plus, on every editor created under that host, zh-CN descriptions for the six command-palette actions Monaco describes in English literals that no catalog can reach (Fold, Unfold, Find with Arguments, Refactor..., Source Action..., Paste As...), each held to the installed `monaco-editor` by an upgrade-tripwire test. Idempotent — every caller gets the same module; a load failure is not cached, so a fresh call retries.
|
|
10
10
|
- **`getWorker` extension point** — plug third-party language workers (e.g. monaco-yaml) into the routing; return `undefined` to fall back to the built-in routes.
|
|
11
11
|
- **`monacoLanguages`** — the sorted catalog of every built-in language id the installed Monaco registers (with the `MonacoBuiltInLanguage` union type behind it), held to the installed `monaco-editor` by an upgrade-tripwire test. These are *ids*, not display names (`proto`, `sol`, `aes`); an unknown id silently falls back to `plaintext`.
|
|
12
12
|
- **`disabledKeybindings` / `installDisabledKeybindings`** — swallow the find/replace chords and the command palette's F1 for editors that opt in. Monaco's standalone keybinding service is page-global and its rules are permanent, so the install registers each chord once per monaco module's service (duplicate copies of this package share the ledger; independent monaco bundles register their own) and scopes it with a per-capability context key created on the opted-in editor; the chord table is held to the installed `monaco-editor` by an upgrade-tripwire test.
|
|
@@ -15,9 +15,10 @@ Part of [inkstone](https://github.com/coldsmirk/inkstone). For a drop-in React c
|
|
|
15
15
|
- **Embedded-editor fixes** — works around [microsoft/monaco-editor#5177](https://github.com/microsoft/monaco-editor/issues/5177) (find-widget button tooltips landing on top of the buttons they describe) with a `width: max-content` hover-layer style tagged `data-inkstone="monaco-hover-geometry"`; deleted the moment upstream fixes it. The tooltip layer positions against the editor's container — give that container `position: relative`. The command palette's row tooltips have the same trouble inside the palette: 0.56 positions them against the palette's list, which sets no position of its own, so they landed above the row they describe (by the height of the filter box); a `position: relative` style on the list, tagged `data-inkstone="monaco-quick-input-geometry"`, puts them on their row until upstream does.
|
|
16
16
|
- **`StreamingEditorController`** — stream generated text into an editor without routing every token through app state: `begin()` / `append(chunk)` / `end()` / `discard()`, `attach(editor)` / `detach(editor)` with buffered replay for late-mounting editors (and for a model swapped onto the attached editor mid-stream, including after the final chunk), and `isStreaming()` for the onChange guard. The stream locks every editor it touches read-only and `end()` / `discard()` restore each one's own setting — by handle, so an editor detached mid-stream is restored too; `discard()` puts every displaced document back into the model it came from, and `end()` does the same for every model except the one keeping the streamed text. Full-buffer writes and restorations preserve each model's UTF-8 BOM. Editors and models the app disposed mid-stream are skipped, never touched — teardown can race the stream without wedging it.
|
|
17
17
|
- **`replaceInEditor(editor, old, new, all)`** — exact find-and-replace on an editor handle via `executeEdits`, preserving undo/cursor/folds; returns whether the edit landed (`false` = no editor / empty search / no match / a read-only editor refused it — e.g. one locked by a stream in progress), so a tool-call handler knows to fall back to a state write.
|
|
18
|
-
- **`registerShikiHighlighting(monaco, highlighter)`** — wire `@coldsmirk/inkstone-core`'s shared Shiki highlighter onto a monaco module, once per distinct module: defines the highlighter's themes as Monaco themes, registers each loaded grammar's canonical name as a Monaco language where Monaco has none and
|
|
19
|
-
- **
|
|
20
|
-
-
|
|
18
|
+
- **`registerShikiHighlighting(monaco, highlighter)`** — wire `@coldsmirk/inkstone-core`'s shared Shiki highlighter onto a monaco module (one from `ensureMonacoHost`), once per distinct module: defines the highlighter's themes as Monaco themes, registers each loaded grammar's canonical name as a Monaco language where Monaco has none and its Shiki tokenization in Monaco's registry — over Monaco's own, JSON's included (its mode's tokenizer is turned off through its configuration, its other features left on) — and patches `setTheme` so the Shiki theme ids route through the highlighter while non-Shiki ids (vs/vs-dark, caller-defined) keep Monaco's original path. Called again on a bridged module, it registers the grammars loaded since. A failed registration is restored and retried on the next call.
|
|
19
|
+
- **Tokenization off the page's thread** — VS Code's design, through the same door in Monaco: each model an editor shows is tokenized whole in a worker of its own, built from what the shared highlighter was built from (`highlighterSources`), the tokens and the state each line ends in sent back as they are made and moved past the edits the worker has not yet seen. The page's thread tokenizes only what Monaco still asks of it before the worker reaches it — a model's first lines in view, the lines an edit touches — and of those only a line it can afford: one of 1,000 characters or more, or one the grammar does not finish in 50 ms, is drawn in the default color until the worker's tokens arrive, the line after it read from the state before it (a minified line costs the grammar about 10 ms per 1,000 characters, 370 over 10,000, and one in view would hold the page that long). The worker bounds a line as VS Code does, at 20,000 characters and 500 ms. Where the page passes a line over in the lines it paints first, its own states for the lines after it stand — Monaco keeps a state it computed over one sent later — so an edit there is drawn from them until the worker's tokens arrive. A highlighter inkstone did not build, or a host that routes no worker for `textmate`, leaves every model to Monaco's own pass on the page's thread, under the same bound.
|
|
20
|
+
- **Which regex engine** — the worker builds the engine kind the shared highlighter was configured with: Oniguruma wherever an engine was configured, the strict JavaScript engine where none was, so the page and the worker tokenize alike. The JavaScript engine translates each grammar's patterns at the first tokenization — in the worker for a model, and on the page for the lines it tokenizes itself (a TypeScript file's first sixty lines: about 170 ms) — so an app that opens files in Monaco should configure `onigurumaEngine` from `@coldsmirk/inkstone-core` (`configureHighlighter({ engine: onigurumaEngine })`, before the first `getHighlighter()`): its WASM is inlined into your bundle and compiles the same lines in about 20 ms.
|
|
21
|
+
- **`registerShikiLanguage(monaco, highlighter, language)`** — the lazy path: an app that loads grammars as files are opened calls this after each `highlighter.loadLanguage(...)` and before a model names the language. Registers the language where Monaco lacks it (`toml`, `shellscript`), its tokenization once under the grammar's canonical name (an alias like `ts` collapses), the models of a grammar shiki has built again since (a language it embeds lazily arrived) tokenized again under the new one, and leaves the theme the editor shows alone — the one thing re-running `@shikijs/monaco`'s `shikiToMonaco` cannot promise, which is why the bridge is inkstone's own. Throws for a grammar the highlighter has not loaded.
|
|
21
22
|
- **`installDesignTokens()`** — defines the VS Code design tokens monaco-editor 0.56's chrome reads and the standalone editor never does ([microsoft/monaco-editor#5408](https://github.com/microsoft/monaco-editor/issues/5408)): `--vscode-cornerRadius-{small,medium,large,xLarge}` and `--vscode-shadow-{md,lg,xl}`, on `.monaco-editor` (the context menu's shadow root and a host's `overflowWidgetsDomNode` inherit them). Without them the context menu, completion, hover, and find widgets ship square and shadowless. Each resolves through the host's `--inkstone-radius-*` / `--inkstone-shadow-*` token first (see `@coldsmirk/inkstone-core`), then a fallback scale in the spirit of 0.55's look. It also keeps an input box's focus ring single: the standalone editor outlines both the `.monaco-inputbox` container (its `synthetic-focus` ring, which follows the box's radius) and the inner `<input>` (square, 1px inside), and the inner ring's corners poke out as soon as the box has corners — the container's ring is the one ring. `ensureMonacoHost` installs it; idempotent.
|
|
22
23
|
- **`DEFAULT_MONACO_OPTIONS`** — the inkstone construction-option baseline (no minimap, 14px/1.6 type on the shared `--inkstone-font-family-code` token, embedded-friendly find widget, tuned completion, thin scrollbars). Spread it to extend: `{ ...DEFAULT_MONACO_OPTIONS, wordWrap: "on" }`.
|
|
23
24
|
- **`reconcileModelValue` / `modelValueMatches`** — the controlled-value reconcile: diff an external value against a live model (normalized to the model's EOL) and apply one minimal `applyEdits` replace that never lands on the undo stack or moves a caret outside the changed region.
|
|
@@ -28,7 +29,7 @@ Part of [inkstone](https://github.com/coldsmirk/inkstone). For a drop-in React c
|
|
|
28
29
|
pnpm add @coldsmirk/inkstone-monaco monaco-editor
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
`monaco-editor` is a **peer dependency** — your app owns the version. Node.js >= 24 for build / SSR hosts. Workers are bundled from dependency-relative `new Worker(new URL(...))` references; Vite and webpack 5 handle this form natively. Plain Rollup does not emit these workers by itself, so configure worker/asset handling that bundles the referenced modules or implement `getWorker` for every label your app can request.
|
|
32
|
+
`monaco-editor` is a **peer dependency** — your app owns the version. Node.js >= 24 for build / SSR hosts. Workers are bundled from dependency-relative `new Worker(new URL(...))` references — monaco's five and inkstone's tokenization worker (`workers/textmate.worker.js`, under the `textmate` label); Vite and webpack 5 handle this form natively. Plain Rollup does not emit these workers by itself, so configure worker/asset handling that bundles the referenced modules or implement `getWorker` for every label your app can request.
|
|
32
33
|
|
|
33
34
|
## Quick start
|
|
34
35
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { INITIAL } from "@shikijs/vscode-textmate";
|
|
2
|
+
//#region src/tokenization/content.ts
|
|
3
|
+
const LINE_BREAK = /\r\n|\r|\n/;
|
|
4
|
+
function lineCount(text) {
|
|
5
|
+
let count = 1;
|
|
6
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
7
|
+
const char = text.codePointAt(index);
|
|
8
|
+
if (char === 10) count += 1;
|
|
9
|
+
else if (char === 13) {
|
|
10
|
+
count += 1;
|
|
11
|
+
if (text.codePointAt(index + 1) === 10) index += 1;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return count;
|
|
15
|
+
}
|
|
16
|
+
function lineSplice({ range, text }) {
|
|
17
|
+
return {
|
|
18
|
+
start: range.startLineNumber - 1,
|
|
19
|
+
removed: range.endLineNumber - range.startLineNumber + 1,
|
|
20
|
+
inserted: lineCount(text)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function applySplice(array, { start, removed, inserted }, fill) {
|
|
24
|
+
const RUN = 8192;
|
|
25
|
+
const items = Array.from({ length: inserted }, (_, index) => fill(index));
|
|
26
|
+
array.splice(start, removed, ...items.slice(0, RUN));
|
|
27
|
+
for (let from = RUN; from < items.length; from += RUN) array.splice(start + from, 0, ...items.slice(from, from + RUN));
|
|
28
|
+
}
|
|
29
|
+
function applyChange(lines, change) {
|
|
30
|
+
const { range, text } = change;
|
|
31
|
+
const written = `${(lines[range.startLineNumber - 1] ?? "").slice(0, range.startColumn - 1)}${text}${(lines[range.endLineNumber - 1] ?? "").slice(range.endColumn - 1)}`.split(LINE_BREAK);
|
|
32
|
+
applySplice(lines, lineSplice(change), (index) => written[index] ?? "");
|
|
33
|
+
}
|
|
34
|
+
function lineAfter(line, edits) {
|
|
35
|
+
let at = line;
|
|
36
|
+
for (const { changes } of edits) for (const change of changes) {
|
|
37
|
+
const { start, removed, inserted } = lineSplice(change);
|
|
38
|
+
if (at - 1 >= start + removed) at += inserted - removed;
|
|
39
|
+
else if (at - 1 >= start) return;
|
|
40
|
+
}
|
|
41
|
+
return at;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/tokenization/states.ts
|
|
45
|
+
const Stack = INITIAL.constructor;
|
|
46
|
+
function stateDiff(previous, next) {
|
|
47
|
+
let pops = 0;
|
|
48
|
+
const newFrames = [];
|
|
49
|
+
let first = previous;
|
|
50
|
+
let second = next;
|
|
51
|
+
while (first !== second) if (first && (!second || first.depth >= second.depth)) {
|
|
52
|
+
pops += 1;
|
|
53
|
+
first = first.parent;
|
|
54
|
+
} else if (second) {
|
|
55
|
+
newFrames.push(second.toStateStackFrame());
|
|
56
|
+
second = second.parent;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
pops,
|
|
60
|
+
newFrames: newFrames.toReversed()
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function applyStateDiff(previous, diff) {
|
|
64
|
+
let stack = previous;
|
|
65
|
+
for (let pop = 0; pop < diff.pops; pop += 1) {
|
|
66
|
+
if (!stack) return null;
|
|
67
|
+
stack = stack.parent;
|
|
68
|
+
}
|
|
69
|
+
for (const frame of diff.newFrames) stack = Stack.pushFrame(stack, frame);
|
|
70
|
+
return stack;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/tokenization/metadata.ts
|
|
74
|
+
const LANGUAGE_MASK = 255;
|
|
75
|
+
const KEPT_MASK = 32512;
|
|
76
|
+
const FOREGROUND_MASK = 16744448;
|
|
77
|
+
const FOREGROUND_OFFSET = 15;
|
|
78
|
+
const BACKGROUND_OFFSET = 24;
|
|
79
|
+
const DEFAULT_FOREGROUND = 1;
|
|
80
|
+
const DEFAULT_BACKGROUND = 2;
|
|
81
|
+
function hexOf(color) {
|
|
82
|
+
const hex = color.trim().toLowerCase();
|
|
83
|
+
return hex.length === 9 && hex.endsWith("ff") ? hex.slice(0, 7) : hex;
|
|
84
|
+
}
|
|
85
|
+
function channel(value) {
|
|
86
|
+
return Math.round(value).toString(16).padStart(2, "0");
|
|
87
|
+
}
|
|
88
|
+
function hexOfMapColor({ rgba }) {
|
|
89
|
+
const opaque = `#${channel(rgba.r)}${channel(rgba.g)}${channel(rgba.b)}`;
|
|
90
|
+
return rgba.a >= 1 ? opaque : `${opaque}${channel(rgba.a * 255)}`;
|
|
91
|
+
}
|
|
92
|
+
function colorIdsFor(highlighterColors, monacoColors) {
|
|
93
|
+
const monacoIds = /* @__PURE__ */ new Map();
|
|
94
|
+
const colors = monacoColors ?? [];
|
|
95
|
+
for (const [id, color] of colors.entries()) if (color && !monacoIds.has(hexOfMapColor(color))) monacoIds.set(hexOfMapColor(color), id);
|
|
96
|
+
return Uint16Array.from(highlighterColors, (color) => monacoIds.get(hexOf(color ?? "")) ?? DEFAULT_FOREGROUND);
|
|
97
|
+
}
|
|
98
|
+
function monacoMetadata(metadata, languageId, colorIds) {
|
|
99
|
+
const foreground = colorIds[(metadata & FOREGROUND_MASK) >>> FOREGROUND_OFFSET] ?? DEFAULT_FOREGROUND;
|
|
100
|
+
return (languageId & LANGUAGE_MASK | metadata & KEPT_MASK | foreground << FOREGROUND_OFFSET | DEFAULT_BACKGROUND << BACKGROUND_OFFSET) >>> 0;
|
|
101
|
+
}
|
|
102
|
+
function defaultMetadata(languageId) {
|
|
103
|
+
return (languageId & LANGUAGE_MASK | 33587200) >>> 0;
|
|
104
|
+
}
|
|
105
|
+
function toMonacoTokens(tokens, languageId, colorIds) {
|
|
106
|
+
for (let index = 1; index < tokens.length; index += 2) tokens[index] = monacoMetadata(tokens[index] ?? 0, languageId, colorIds);
|
|
107
|
+
return tokens;
|
|
108
|
+
}
|
|
109
|
+
function withEndOffsets(tokens, lineLength) {
|
|
110
|
+
const last = tokens.length - 2;
|
|
111
|
+
for (let index = 0; index < last; index += 2) tokens[index] = tokens[index + 2] ?? lineLength;
|
|
112
|
+
if (last >= 0) tokens[last] = lineLength;
|
|
113
|
+
return tokens;
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/tokenization/bounds.ts
|
|
117
|
+
const SYNC_LINE_BOUND = 1e3;
|
|
118
|
+
const WORKER_LINE_BOUND = 2e4;
|
|
119
|
+
//#endregion
|
|
120
|
+
export { toMonacoTokens as a, stateDiff as c, lineAfter as d, lineSplice as f, defaultMetadata as i, applyChange as l, WORKER_LINE_BOUND as n, withEndOffsets as o, colorIdsFor as r, applyStateDiff as s, SYNC_LINE_BOUND as t, applySplice as u };
|
package/dist/index.d.ts
CHANGED
|
@@ -33,8 +33,9 @@ interface MonacoHostOptions {
|
|
|
33
33
|
*/
|
|
34
34
|
type MonacoModule = typeof monacoEditor;
|
|
35
35
|
/**
|
|
36
|
-
* Bring up the shared Monaco host: UI locale, then the monaco module
|
|
37
|
-
* fully offline, no CDN, workers bundled by
|
|
36
|
+
* Bring up the shared Monaco host: UI locale, then the monaco module and the parts of it the
|
|
37
|
+
* Shiki tokenization stands on, then worker routing — fully offline, no CDN, workers bundled by
|
|
38
|
+
* the consumer's bundler.
|
|
38
39
|
*
|
|
39
40
|
* Idempotent and async-safe: every caller shares one promise, and the first call wins the
|
|
40
41
|
* configuration (`globalThis.MonacoEnvironment` is a single global — per-editor configuration
|
|
@@ -249,16 +250,25 @@ declare function replaceInEditor(target: editor.ICodeEditor | null, search: stri
|
|
|
249
250
|
//#endregion
|
|
250
251
|
//#region src/shiki-bridge.d.ts
|
|
251
252
|
/**
|
|
252
|
-
* Register the shared Shiki highlighter's languages, themes, and
|
|
253
|
-
* once per distinct module, whichever caller gets there first — and, on a module already
|
|
254
|
-
* bridged,
|
|
253
|
+
* Register the shared Shiki highlighter's languages, themes, and tokenization on a monaco module
|
|
254
|
+
* — once per distinct module, whichever caller gets there first — and, on a module already
|
|
255
|
+
* bridged, the tokenization of any grammar loaded since. Language services are unaffected:
|
|
255
256
|
* Shiki replaces syntax highlighting only. The module is recorded only after every registration
|
|
256
|
-
* succeeds, so a transient failure can retry on a later call.
|
|
257
|
+
* succeeds, so a transient failure can retry on a later call. The module must be the host's
|
|
258
|
+
* (`ensureMonacoHost`), which brings up the parts of Monaco the tokenization stands on.
|
|
259
|
+
*
|
|
260
|
+
* A model is tokenized whole in a worker of its own, built from what the highlighter was built
|
|
261
|
+
* from (`@coldsmirk/inkstone-core`'s `highlighterSources`) — VS Code's design, through the same
|
|
262
|
+
* door in Monaco: the page's thread tokenizes only the lines Monaco asks of it before the worker
|
|
263
|
+
* reaches them — a model's first lines in view, the lines an edit touches — and of those only a
|
|
264
|
+
* line it can afford; a longer one waits for the worker's colours (`tokenization/bounds.ts`).
|
|
265
|
+
* A highlighter inkstone did not build, or a host that routes no worker for the `textmate`
|
|
266
|
+
* label, leaves every model to Monaco's own pass on the page's thread, under the same bound.
|
|
257
267
|
*
|
|
258
268
|
* Monaco's own `setTheme` is patched to route the Shiki theme ids through the highlighter, so
|
|
259
|
-
* the
|
|
260
|
-
* caller-defined themes keep Monaco's original path. `create` is patched so its `theme`
|
|
261
|
-
* reaches that patch. The shape is `@shikijs/monaco`'s, kept here because that package
|
|
269
|
+
* the tokenization paints in the theme the editor shows; Monaco's built-in themes (vs/vs-dark)
|
|
270
|
+
* and caller-defined themes keep Monaco's original path. `create` is patched so its `theme`
|
|
271
|
+
* option reaches that patch. The shape is `@shikijs/monaco`'s, kept here because that package
|
|
262
272
|
* installs tokenizers only for the grammars loaded at the moment it is called, and re-calling
|
|
263
273
|
* it for a grammar loaded later resets the editor's theme to the first one.
|
|
264
274
|
*
|
|
@@ -267,12 +277,13 @@ declare function replaceInEditor(target: editor.ICodeEditor | null, search: stri
|
|
|
267
277
|
*/
|
|
268
278
|
declare function registerShikiHighlighting(monaco: MonacoModule, highlighter: HighlighterCore): void;
|
|
269
279
|
/**
|
|
270
|
-
*
|
|
271
|
-
* — the lazy path: an app that loads grammars as files are opened registers each here,
|
|
272
|
-
* its `loadLanguage`, and before a model names it. `language` may be an alias (`ts`); it
|
|
280
|
+
* Register the tokenization of one grammar the highlighter has loaded since the module was
|
|
281
|
+
* bridged — the lazy path: an app that loads grammars as files are opened registers each here,
|
|
282
|
+
* after its `loadLanguage`, and before a model names it. `language` may be an alias (`ts`); it
|
|
273
283
|
* collapses to the grammar's canonical name. Bridges the module first where nothing has, and
|
|
274
|
-
* is nothing for a grammar already registered
|
|
275
|
-
*
|
|
284
|
+
* is nothing for a grammar already registered — but a grammar shiki has built again since, as
|
|
285
|
+
* a language it embeds lazily arrived, has its models tokenized again under the new one. Throws
|
|
286
|
+
* when the highlighter has not loaded the grammar: there is nothing to tokenize with.
|
|
276
287
|
*/
|
|
277
288
|
declare function registerShikiLanguage(monaco: MonacoModule, highlighter: HighlighterCore, language: string): void;
|
|
278
289
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as toMonacoTokens, d as lineAfter, i as defaultMetadata, r as colorIdsFor, s as applyStateDiff } from "./bounds-CZ_iAUoc.js";
|
|
2
|
+
import { CODE_FONT_FAMILY, highlighterSources, inkstoneRadius, inkstoneShadow } from "@coldsmirk/inkstone-core";
|
|
2
3
|
import { EncodedTokenMetadata, INITIAL } from "@shikijs/vscode-textmate";
|
|
3
4
|
//#region src/auto-imports.ts
|
|
4
5
|
const MISSING_LOOKUP_REJECTION$1 = "Missing requestHandler or method: getAutoImportCompletionsAtPosition";
|
|
@@ -214,6 +215,33 @@ function installHoverFocusFix() {
|
|
|
214
215
|
document.head.append(style);
|
|
215
216
|
}
|
|
216
217
|
//#endregion
|
|
218
|
+
//#region src/tokenization/internals.ts
|
|
219
|
+
async function loadMonacoInternals() {
|
|
220
|
+
const [languages, tokens] = await Promise.all([import("monaco-editor/editor/common/languages.js"), import("monaco-editor/editor/common/tokens/contiguousMultilineTokensBuilder.js")]);
|
|
221
|
+
return {
|
|
222
|
+
registry: languages.TokenizationRegistry,
|
|
223
|
+
encoded: (encoded, endState) => new languages.EncodedTokenizationResult(encoded, [], endState),
|
|
224
|
+
classic: (classic, language, endState) => new languages.TokenizationResult(classic.map((token) => new languages.Token(token.offset, token.type, language)), endState),
|
|
225
|
+
multiline: (lines) => {
|
|
226
|
+
const builder = new tokens.ContiguousMultilineTokensBuilder();
|
|
227
|
+
for (const [lineNumber, lineTokens] of lines) builder.add(lineNumber, lineTokens);
|
|
228
|
+
return builder.finalize();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const INTERNALS_KEY = Symbol.for("coldsmirk.inkstone.monaco.internals");
|
|
233
|
+
function ledger() {
|
|
234
|
+
const host = globalThis;
|
|
235
|
+
host[INTERNALS_KEY] ??= /* @__PURE__ */ new WeakMap();
|
|
236
|
+
return host[INTERNALS_KEY];
|
|
237
|
+
}
|
|
238
|
+
function rememberInternals(monaco, internals) {
|
|
239
|
+
ledger().set(monaco, internals);
|
|
240
|
+
}
|
|
241
|
+
function internalsOf(monaco) {
|
|
242
|
+
return ledger().get(monaco);
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
217
245
|
//#region src/host.ts
|
|
218
246
|
const HOST_PROMISE_KEY = Symbol.for("coldsmirk.inkstone.monaco.hostPromise");
|
|
219
247
|
const NLS_REGISTRY_KEY = Symbol.for("coldsmirk.inkstone.monaco.nlsRegistry");
|
|
@@ -254,11 +282,13 @@ async function applyZhCnCatalog() {
|
|
|
254
282
|
function editorWorker() {
|
|
255
283
|
return new Worker(new URL("workers/editor.worker.js", import.meta.url), { type: "module" });
|
|
256
284
|
}
|
|
285
|
+
const TEXTMATE_WORKER_LABEL = "textmate";
|
|
257
286
|
const WORKER_BY_LABEL = {
|
|
258
287
|
typescript: () => new Worker(new URL("workers/ts.worker.js", import.meta.url), { type: "module" }),
|
|
259
288
|
json: () => new Worker(new URL("workers/json.worker.js", import.meta.url), { type: "module" }),
|
|
260
289
|
css: () => new Worker(new URL("workers/css.worker.js", import.meta.url), { type: "module" }),
|
|
261
|
-
html: () => new Worker(new URL("workers/html.worker.js", import.meta.url), { type: "module" })
|
|
290
|
+
html: () => new Worker(new URL("workers/html.worker.js", import.meta.url), { type: "module" }),
|
|
291
|
+
[TEXTMATE_WORKER_LABEL]: () => new Worker(new URL("workers/textmate.worker.js", import.meta.url), { type: "module" })
|
|
262
292
|
};
|
|
263
293
|
const WORKER_ALIASES = {
|
|
264
294
|
javascript: "typescript",
|
|
@@ -286,6 +316,7 @@ function ensureMonacoHost(options = {}) {
|
|
|
286
316
|
installQuickInputGeometryFix();
|
|
287
317
|
installHoverFocusFix();
|
|
288
318
|
const monaco = await import("monaco-editor");
|
|
319
|
+
rememberInternals(monaco, await loadMonacoInternals());
|
|
289
320
|
globalThis.MonacoEnvironment = { getWorker: (workerId, label) => getWorker?.(workerId, label) ?? builtinGetWorker(label) };
|
|
290
321
|
if (locale === "zh-cn") monaco.editor.onDidCreateEditor(localizeActionDescriptions);
|
|
291
322
|
return monaco;
|
|
@@ -600,6 +631,280 @@ function replaceInEditor(target, search, replaceWith, all) {
|
|
|
600
631
|
return target.executeEdits("inkstone-replace", edits);
|
|
601
632
|
}
|
|
602
633
|
//#endregion
|
|
634
|
+
//#region src/tokenization/controller.ts
|
|
635
|
+
var WorkerTokenizerController = class {
|
|
636
|
+
model;
|
|
637
|
+
store;
|
|
638
|
+
internals;
|
|
639
|
+
channel;
|
|
640
|
+
attachment;
|
|
641
|
+
session;
|
|
642
|
+
disposed = false;
|
|
643
|
+
language;
|
|
644
|
+
constructor(options) {
|
|
645
|
+
this.language = options.language;
|
|
646
|
+
this.model = options.model;
|
|
647
|
+
this.store = options.store;
|
|
648
|
+
this.internals = options.internals;
|
|
649
|
+
this.channel = options.channel;
|
|
650
|
+
this.attachment = this.model.onDidChangeAttached(() => {
|
|
651
|
+
this.follow();
|
|
652
|
+
});
|
|
653
|
+
queueMicrotask(() => {
|
|
654
|
+
this.follow();
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
follow() {
|
|
658
|
+
if (this.disposed) return;
|
|
659
|
+
const attached = this.model.isAttachedToEditor();
|
|
660
|
+
if (attached && !this.session) this.connect();
|
|
661
|
+
else if (!attached && this.session) this.disconnect();
|
|
662
|
+
}
|
|
663
|
+
connect() {
|
|
664
|
+
const id = this.channel.open(this, this.model.getLinesContent(), this.model.getVersionId());
|
|
665
|
+
const pending = [];
|
|
666
|
+
const listener = this.model.onDidChangeContent((event) => {
|
|
667
|
+
const changes = event.changes.map(({ range, text }) => {
|
|
668
|
+
return {
|
|
669
|
+
range: {
|
|
670
|
+
startLineNumber: range.startLineNumber,
|
|
671
|
+
startColumn: range.startColumn,
|
|
672
|
+
endLineNumber: range.endLineNumber,
|
|
673
|
+
endColumn: range.endColumn
|
|
674
|
+
},
|
|
675
|
+
text
|
|
676
|
+
};
|
|
677
|
+
});
|
|
678
|
+
pending.push({
|
|
679
|
+
version: event.versionId,
|
|
680
|
+
changes
|
|
681
|
+
});
|
|
682
|
+
this.channel.send({
|
|
683
|
+
kind: "change",
|
|
684
|
+
model: id,
|
|
685
|
+
version: event.versionId,
|
|
686
|
+
changes
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
this.session = {
|
|
690
|
+
id,
|
|
691
|
+
pending,
|
|
692
|
+
listener
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
disconnect() {
|
|
696
|
+
if (!this.session) return;
|
|
697
|
+
this.session.listener.dispose();
|
|
698
|
+
this.channel.close(this.session.id);
|
|
699
|
+
this.session = void 0;
|
|
700
|
+
}
|
|
701
|
+
keepStates(message) {
|
|
702
|
+
let previous = null;
|
|
703
|
+
for (const [index, line] of message.lines.entries()) {
|
|
704
|
+
const diff = message.states[index];
|
|
705
|
+
const follows = index > 0 && message.lines[index - 1] === line - 1;
|
|
706
|
+
let state = null;
|
|
707
|
+
if (diff && (!follows || previous)) try {
|
|
708
|
+
state = applyStateDiff(follows ? previous : null, diff);
|
|
709
|
+
} catch {
|
|
710
|
+
state = null;
|
|
711
|
+
}
|
|
712
|
+
if (state) this.store.setEndState(line, state);
|
|
713
|
+
previous = state;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
receiveTokens(message) {
|
|
717
|
+
const { session } = this;
|
|
718
|
+
if (session?.id !== message.model) return;
|
|
719
|
+
const { pending } = session;
|
|
720
|
+
while (pending[0] && pending[0].version <= message.version) pending.shift();
|
|
721
|
+
const lines = [];
|
|
722
|
+
let offset = 0;
|
|
723
|
+
for (const [index, sent] of message.lines.entries()) {
|
|
724
|
+
const length = message.lengths[index] ?? 0;
|
|
725
|
+
const tokens = message.tokens.subarray(offset, offset + length);
|
|
726
|
+
const line = lineAfter(sent, pending);
|
|
727
|
+
offset += length;
|
|
728
|
+
if (line !== void 0) lines.push([line, tokens]);
|
|
729
|
+
}
|
|
730
|
+
if (lines.length > 0) this.store.setTokens(this.internals.multiline(lines));
|
|
731
|
+
if (pending.length === 0) this.keepStates(message);
|
|
732
|
+
}
|
|
733
|
+
finished(model, version) {
|
|
734
|
+
const { session } = this;
|
|
735
|
+
if (session?.id === model && session.pending.length === 0 && version === this.model.getVersionId()) this.store.backgroundTokenizationFinished();
|
|
736
|
+
}
|
|
737
|
+
requestTokens(startLineNumber, endLineNumberExclusive) {
|
|
738
|
+
if (this.session) this.channel.send({
|
|
739
|
+
kind: "retokenize",
|
|
740
|
+
model: this.session.id,
|
|
741
|
+
start: startLineNumber,
|
|
742
|
+
end: endLineNumberExclusive
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
dispose() {
|
|
746
|
+
if (this.disposed) return;
|
|
747
|
+
this.disposed = true;
|
|
748
|
+
this.attachment.dispose();
|
|
749
|
+
this.disconnect();
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region src/tokenization/client.ts
|
|
754
|
+
var TokenizationClient = class {
|
|
755
|
+
options;
|
|
756
|
+
controllers = /* @__PURE__ */ new Map();
|
|
757
|
+
worker;
|
|
758
|
+
sentLanguages = 0;
|
|
759
|
+
nextModel = 1;
|
|
760
|
+
dead = false;
|
|
761
|
+
constructor(options) {
|
|
762
|
+
this.options = options;
|
|
763
|
+
}
|
|
764
|
+
start() {
|
|
765
|
+
if (this.dead) return false;
|
|
766
|
+
if (this.worker) return true;
|
|
767
|
+
let created;
|
|
768
|
+
try {
|
|
769
|
+
created = this.options.createWorker();
|
|
770
|
+
} catch {
|
|
771
|
+
this.dead = true;
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
this.worker = Promise.resolve(created);
|
|
775
|
+
this.worker.then((worker) => {
|
|
776
|
+
worker.addEventListener("message", (event) => {
|
|
777
|
+
this.receive(event.data);
|
|
778
|
+
});
|
|
779
|
+
worker.addEventListener("error", () => {
|
|
780
|
+
this.fail();
|
|
781
|
+
});
|
|
782
|
+
}, () => {
|
|
783
|
+
this.fail();
|
|
784
|
+
});
|
|
785
|
+
const { sources } = this.options;
|
|
786
|
+
this.sentLanguages = sources.languages.length;
|
|
787
|
+
this.post({
|
|
788
|
+
kind: "build",
|
|
789
|
+
engine: sources.engine,
|
|
790
|
+
themes: this.options.themes(),
|
|
791
|
+
languages: [...sources.languages]
|
|
792
|
+
});
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
sendLanguages() {
|
|
796
|
+
const { languages } = this.options.sources;
|
|
797
|
+
if (languages.length > this.sentLanguages) {
|
|
798
|
+
this.post({
|
|
799
|
+
kind: "languages",
|
|
800
|
+
languages: languages.slice(this.sentLanguages)
|
|
801
|
+
});
|
|
802
|
+
this.sentLanguages = languages.length;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
post(message) {
|
|
806
|
+
this.worker?.then((worker) => {
|
|
807
|
+
worker.postMessage(message);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
receive(message) {
|
|
811
|
+
switch (message.kind) {
|
|
812
|
+
case "tokens":
|
|
813
|
+
this.controllers.get(message.model)?.receiveTokens(message);
|
|
814
|
+
break;
|
|
815
|
+
case "finished":
|
|
816
|
+
this.controllers.get(message.model)?.finished(message.model, message.version);
|
|
817
|
+
break;
|
|
818
|
+
case "unavailable": {
|
|
819
|
+
const controller = this.controllers.get(message.model);
|
|
820
|
+
if (controller) this.options.unavailable([controller.language]);
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
case "failed": this.fail();
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
fail() {
|
|
827
|
+
if (this.dead) return;
|
|
828
|
+
this.dead = true;
|
|
829
|
+
this.worker?.then((worker) => {
|
|
830
|
+
worker.terminate();
|
|
831
|
+
});
|
|
832
|
+
this.worker = void 0;
|
|
833
|
+
this.options.unavailable("all");
|
|
834
|
+
}
|
|
835
|
+
open(model, store, tokenization) {
|
|
836
|
+
if (!this.start()) return;
|
|
837
|
+
return new WorkerTokenizerController({
|
|
838
|
+
language: tokenization.language,
|
|
839
|
+
model,
|
|
840
|
+
store,
|
|
841
|
+
internals: this.options.internals,
|
|
842
|
+
channel: {
|
|
843
|
+
open: (controller, lines, version) => {
|
|
844
|
+
const id = this.nextModel;
|
|
845
|
+
this.nextModel += 1;
|
|
846
|
+
this.sendLanguages();
|
|
847
|
+
this.controllers.set(id, controller);
|
|
848
|
+
this.post({
|
|
849
|
+
kind: "open",
|
|
850
|
+
model: id,
|
|
851
|
+
...tokenization,
|
|
852
|
+
version,
|
|
853
|
+
lines
|
|
854
|
+
});
|
|
855
|
+
return id;
|
|
856
|
+
},
|
|
857
|
+
send: (message) => {
|
|
858
|
+
this.post(message);
|
|
859
|
+
},
|
|
860
|
+
close: (id) => {
|
|
861
|
+
this.controllers.delete(id);
|
|
862
|
+
this.post({
|
|
863
|
+
kind: "close",
|
|
864
|
+
model: id
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/tokenization/support.ts
|
|
873
|
+
function shikiTokenization(options) {
|
|
874
|
+
const { internals, highlighter, language, languageId, theme, colorIds, scopeOf, background } = options;
|
|
875
|
+
const tokenizeOnPage = (line, state) => {
|
|
876
|
+
if (line.length >= 1e3) return;
|
|
877
|
+
try {
|
|
878
|
+
highlighter.setTheme(theme());
|
|
879
|
+
const result = highlighter.getLanguage(language).tokenizeLine2(line, state, 50);
|
|
880
|
+
return result.stoppedEarly ? void 0 : result;
|
|
881
|
+
} catch {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
return {
|
|
886
|
+
getInitialState: () => INITIAL,
|
|
887
|
+
tokenizeEncoded: (line, _hasEOL, state) => {
|
|
888
|
+
const result = tokenizeOnPage(line, state);
|
|
889
|
+
return result ? internals.encoded(toMonacoTokens(result.tokens, languageId, colorIds()), result.ruleStack) : internals.encoded(Uint32Array.of(0, defaultMetadata(languageId)), state);
|
|
890
|
+
},
|
|
891
|
+
tokenize: (line, _hasEOL, state) => {
|
|
892
|
+
const result = tokenizeOnPage(line, state);
|
|
893
|
+
if (!result) return internals.classic([{
|
|
894
|
+
offset: 0,
|
|
895
|
+
type: ""
|
|
896
|
+
}], language, state);
|
|
897
|
+
const tokens = [];
|
|
898
|
+
for (let index = 0; index < result.tokens.length; index += 2) tokens.push({
|
|
899
|
+
offset: result.tokens[index] ?? 0,
|
|
900
|
+
type: scopeOf(result.tokens[index + 1] ?? 0)
|
|
901
|
+
});
|
|
902
|
+
return internals.classic(tokens, language, result.ruleStack);
|
|
903
|
+
},
|
|
904
|
+
createBackgroundTokenizer: background
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
//#endregion
|
|
603
908
|
//#region src/shiki-bridge.ts
|
|
604
909
|
const SHIKI_REGISTRY_KEY = Symbol.for("coldsmirk.inkstone.monaco.shikiRegistry");
|
|
605
910
|
function bridges() {
|
|
@@ -610,8 +915,6 @@ function bridges() {
|
|
|
610
915
|
host[SHIKI_REGISTRY_KEY] = created;
|
|
611
916
|
return created;
|
|
612
917
|
}
|
|
613
|
-
const TOKENIZE_MAX_LINE_LENGTH = 2e4;
|
|
614
|
-
const TOKENIZE_TIME_LIMIT_MS = 500;
|
|
615
918
|
const ITALIC = 1;
|
|
616
919
|
const BOLD = 2;
|
|
617
920
|
const UNDERLINE = 4;
|
|
@@ -675,6 +978,7 @@ function monacoThemeOf(theme) {
|
|
|
675
978
|
function adoptTheme(bridge, themeName) {
|
|
676
979
|
const { colorMap } = bridge.highlighter.setTheme(themeName);
|
|
677
980
|
const theme = bridge.themes.get(themeName);
|
|
981
|
+
bridge.theme = themeName;
|
|
678
982
|
bridge.colorMap.length = colorMap.length;
|
|
679
983
|
for (const [index, color] of colorMap.entries()) bridge.colorMap[index] = color;
|
|
680
984
|
bridge.scopeOf.clear();
|
|
@@ -686,58 +990,78 @@ function adoptTheme(bridge, themeName) {
|
|
|
686
990
|
if (!bridge.scopeOf.has(key)) bridge.scopeOf.set(key, rule.token);
|
|
687
991
|
}
|
|
688
992
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
};
|
|
701
|
-
function tokensProvider(bridge, lang) {
|
|
702
|
-
return {
|
|
703
|
-
getInitialState() {
|
|
704
|
-
return new TokenizerState(INITIAL);
|
|
705
|
-
},
|
|
706
|
-
tokenize(line, state) {
|
|
707
|
-
if (line.length >= TOKENIZE_MAX_LINE_LENGTH) return {
|
|
708
|
-
endState: state,
|
|
709
|
-
tokens: [{
|
|
710
|
-
startIndex: 0,
|
|
711
|
-
scopes: ""
|
|
712
|
-
}]
|
|
713
|
-
};
|
|
714
|
-
const ruleStack = state instanceof TokenizerState ? state.ruleStack : INITIAL;
|
|
715
|
-
const result = bridge.highlighter.getLanguage(lang).tokenizeLine2(line, ruleStack, TOKENIZE_TIME_LIMIT_MS);
|
|
716
|
-
if (result.stoppedEarly) console.warn(`inkstone: time limit reached when tokenizing a line: ${line.slice(0, 100)}`);
|
|
717
|
-
const tokens = [];
|
|
718
|
-
for (let index = 0; index < result.tokens.length; index += 2) {
|
|
719
|
-
const startIndex = result.tokens[index] ?? 0;
|
|
720
|
-
const metadata = result.tokens[index + 1] ?? 0;
|
|
721
|
-
const color = normalizeColor(bridge.colorMap[EncodedTokenMetadata.getForeground(metadata)]);
|
|
722
|
-
const fontStyle = normalizeFontStyleBits(EncodedTokenMetadata.getFontStyle(metadata));
|
|
723
|
-
const scopes = color ? bridge.scopeOf.get(colorStyleKey(color, fontStyle)) ?? "" : "";
|
|
724
|
-
tokens.push({
|
|
725
|
-
startIndex,
|
|
726
|
-
scopes
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
return {
|
|
730
|
-
endState: new TokenizerState(result.ruleStack),
|
|
731
|
-
tokens
|
|
732
|
-
};
|
|
733
|
-
}
|
|
993
|
+
function scopeOfMetadata(bridge, metadata) {
|
|
994
|
+
const color = normalizeColor(bridge.colorMap[EncodedTokenMetadata.getForeground(metadata)]);
|
|
995
|
+
const fontStyle = normalizeFontStyleBits(EncodedTokenMetadata.getFontStyle(metadata));
|
|
996
|
+
return color ? bridge.scopeOf.get(colorStyleKey(color, fontStyle)) ?? "" : "";
|
|
997
|
+
}
|
|
998
|
+
function colorIds(bridge) {
|
|
999
|
+
const monacoColors = bridge.internals.registry.getColorMap();
|
|
1000
|
+
if (bridge.colorIds?.theme !== bridge.theme || bridge.colorIds.monacoColors !== monacoColors) bridge.colorIds = {
|
|
1001
|
+
theme: bridge.theme,
|
|
1002
|
+
monacoColors,
|
|
1003
|
+
ids: colorIdsFor(bridge.colorMap, monacoColors)
|
|
734
1004
|
};
|
|
1005
|
+
return bridge.colorIds.ids;
|
|
1006
|
+
}
|
|
1007
|
+
function clientOf(bridge) {
|
|
1008
|
+
if (bridge.client !== void 0) return bridge.client;
|
|
1009
|
+
const sources = highlighterSources(bridge.highlighter);
|
|
1010
|
+
const getWorker = globalThis.MonacoEnvironment?.getWorker;
|
|
1011
|
+
bridge.client = sources && getWorker ? new TokenizationClient({
|
|
1012
|
+
internals: bridge.internals,
|
|
1013
|
+
sources,
|
|
1014
|
+
themes: () => bridge.highlighter.getLoadedThemes().map((id) => bridge.highlighter.getTheme(id)),
|
|
1015
|
+
createWorker: () => getWorker("inkstone-textmate", TEXTMATE_WORKER_LABEL),
|
|
1016
|
+
unavailable: (languages) => {
|
|
1017
|
+
if (languages === "all") bridge.client = null;
|
|
1018
|
+
else for (const language of languages) bridge.workerless.add(language);
|
|
1019
|
+
bridge.internals.registry.handleChange(languages === "all" ? bridge.registered.keys().toArray() : [...languages]);
|
|
1020
|
+
}
|
|
1021
|
+
}) : null;
|
|
1022
|
+
return bridge.client;
|
|
1023
|
+
}
|
|
1024
|
+
function quietBuiltInTokenizer(monaco, language) {
|
|
1025
|
+
if (language !== "json") return;
|
|
1026
|
+
const { jsonDefaults } = monaco.json;
|
|
1027
|
+
if (jsonDefaults.modeConfiguration.tokens !== false) jsonDefaults.setModeConfiguration({
|
|
1028
|
+
...jsonDefaults.modeConfiguration,
|
|
1029
|
+
tokens: false
|
|
1030
|
+
});
|
|
735
1031
|
}
|
|
736
1032
|
function install(monaco, bridge, canonical) {
|
|
737
1033
|
if (bridge.registered.has(canonical)) return;
|
|
738
1034
|
if (monaco.languages.getLanguages().every((language) => language.id !== canonical)) monaco.languages.register({ id: canonical });
|
|
739
|
-
monaco
|
|
740
|
-
bridge.registered.
|
|
1035
|
+
quietBuiltInTokenizer(monaco, canonical);
|
|
1036
|
+
bridge.registered.set(canonical, bridge.highlighter.getLanguage(canonical));
|
|
1037
|
+
bridge.internals.registry.register(canonical, shikiTokenization({
|
|
1038
|
+
internals: bridge.internals,
|
|
1039
|
+
highlighter: bridge.highlighter,
|
|
1040
|
+
language: canonical,
|
|
1041
|
+
languageId: monaco.languages.getEncodedLanguageId(canonical),
|
|
1042
|
+
theme: () => bridge.theme,
|
|
1043
|
+
colorIds: () => colorIds(bridge),
|
|
1044
|
+
scopeOf: (metadata) => scopeOfMetadata(bridge, metadata),
|
|
1045
|
+
background: (model, store) => {
|
|
1046
|
+
return (bridge.workerless.has(canonical) ? null : clientOf(bridge))?.open(model, store, {
|
|
1047
|
+
language: canonical,
|
|
1048
|
+
languageId: monaco.languages.getEncodedLanguageId(canonical),
|
|
1049
|
+
theme: bridge.theme,
|
|
1050
|
+
colorIds: colorIds(bridge)
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}));
|
|
1054
|
+
}
|
|
1055
|
+
function retokenizeRebuilt(bridge) {
|
|
1056
|
+
const rebuilt = [];
|
|
1057
|
+
for (const [language, grammar] of bridge.registered) {
|
|
1058
|
+
const current = bridge.highlighter.getLanguage(language);
|
|
1059
|
+
if (current !== grammar) {
|
|
1060
|
+
bridge.registered.set(language, current);
|
|
1061
|
+
rebuilt.push(language);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (rebuilt.length > 0) bridge.internals.registry.handleChange(rebuilt);
|
|
741
1065
|
}
|
|
742
1066
|
function canonicalLanguages(highlighter) {
|
|
743
1067
|
return new Set(highlighter.getLoadedLanguages().map((id) => highlighter.getLanguage(id).name));
|
|
@@ -747,14 +1071,22 @@ function registerShikiHighlighting(monaco, highlighter) {
|
|
|
747
1071
|
const bridged = registry.get(monaco);
|
|
748
1072
|
if (bridged) {
|
|
749
1073
|
for (const canonical of canonicalLanguages(highlighter)) install(monaco, bridged, canonical);
|
|
1074
|
+
retokenizeRebuilt(bridged);
|
|
750
1075
|
return;
|
|
751
1076
|
}
|
|
1077
|
+
const internals = internalsOf(monaco);
|
|
1078
|
+
if (!internals) throw new Error("inkstone: Shiki highlighting needs the monaco module ensureMonacoHost brought up");
|
|
752
1079
|
const bridge = {
|
|
1080
|
+
internals,
|
|
753
1081
|
highlighter,
|
|
754
1082
|
themes: /* @__PURE__ */ new Map(),
|
|
1083
|
+
theme: "",
|
|
755
1084
|
colorMap: [],
|
|
1085
|
+
colorIds: void 0,
|
|
756
1086
|
scopeOf: /* @__PURE__ */ new Map(),
|
|
757
|
-
registered: /* @__PURE__ */ new
|
|
1087
|
+
registered: /* @__PURE__ */ new Map(),
|
|
1088
|
+
client: void 0,
|
|
1089
|
+
workerless: /* @__PURE__ */ new Set()
|
|
758
1090
|
};
|
|
759
1091
|
const originalCreate = monaco.editor.create;
|
|
760
1092
|
const originalSetTheme = monaco.editor.setTheme;
|
|
@@ -791,6 +1123,7 @@ function registerShikiLanguage(monaco, highlighter, language) {
|
|
|
791
1123
|
}
|
|
792
1124
|
if (!bridge) throw new Error("inkstone: the Shiki bridge is not installed on this monaco module");
|
|
793
1125
|
install(monaco, bridge, highlighter.getLanguage(language).name);
|
|
1126
|
+
retokenizeRebuilt(bridge);
|
|
794
1127
|
}
|
|
795
1128
|
//#endregion
|
|
796
1129
|
//#region src/streaming.ts
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { a as toMonacoTokens, c as stateDiff, f as lineSplice, i as defaultMetadata, l as applyChange, o as withEndOffsets, u as applySplice } from "../bounds-CZ_iAUoc.js";
|
|
2
|
+
import { INITIAL } from "@shikijs/vscode-textmate";
|
|
3
|
+
import { createHighlighterCore } from "shiki/core";
|
|
4
|
+
//#region src/tokenization/invalid-lines.ts
|
|
5
|
+
var InvalidLines = class {
|
|
6
|
+
runs = [];
|
|
7
|
+
constructor(lineCount = 0) {
|
|
8
|
+
this.add(0, lineCount);
|
|
9
|
+
}
|
|
10
|
+
first() {
|
|
11
|
+
return this.runs[0]?.start;
|
|
12
|
+
}
|
|
13
|
+
has(line) {
|
|
14
|
+
return this.runs.some((run) => run.start <= line && line < run.end);
|
|
15
|
+
}
|
|
16
|
+
add(start, end) {
|
|
17
|
+
if (end <= start) return;
|
|
18
|
+
let from = start;
|
|
19
|
+
let to = end;
|
|
20
|
+
const kept = this.runs.filter((run) => {
|
|
21
|
+
if (run.end < from || to < run.start) return true;
|
|
22
|
+
from = Math.min(from, run.start);
|
|
23
|
+
to = Math.max(to, run.end);
|
|
24
|
+
return false;
|
|
25
|
+
});
|
|
26
|
+
kept.push({
|
|
27
|
+
start: from,
|
|
28
|
+
end: to
|
|
29
|
+
});
|
|
30
|
+
kept.sort((one, other) => one.start - other.start);
|
|
31
|
+
this.runs.splice(0, this.runs.length, ...kept);
|
|
32
|
+
}
|
|
33
|
+
delete(line) {
|
|
34
|
+
const at = this.runs.findIndex((run) => run.start <= line && line < run.end);
|
|
35
|
+
const run = this.runs[at];
|
|
36
|
+
if (!run) return;
|
|
37
|
+
const pieces = [{
|
|
38
|
+
start: run.start,
|
|
39
|
+
end: line
|
|
40
|
+
}, {
|
|
41
|
+
start: line + 1,
|
|
42
|
+
end: run.end
|
|
43
|
+
}].filter((piece) => piece.start < piece.end);
|
|
44
|
+
this.runs.splice(at, 1, ...pieces);
|
|
45
|
+
}
|
|
46
|
+
splice({ start, removed, inserted }) {
|
|
47
|
+
const shift = inserted - removed;
|
|
48
|
+
const moved = this.runs.flatMap((run) => {
|
|
49
|
+
return [{
|
|
50
|
+
start: run.start,
|
|
51
|
+
end: Math.min(run.end, start)
|
|
52
|
+
}, {
|
|
53
|
+
start: Math.max(run.start, start + removed) + shift,
|
|
54
|
+
end: run.end + shift
|
|
55
|
+
}].filter((piece) => piece.start < piece.end);
|
|
56
|
+
});
|
|
57
|
+
this.runs.splice(0, this.runs.length, ...moved);
|
|
58
|
+
this.add(start, start + inserted);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/tokenization/worker-runtime.ts
|
|
63
|
+
function hasWork(model) {
|
|
64
|
+
return model.requested.length > 0 || model.invalid.first() !== void 0;
|
|
65
|
+
}
|
|
66
|
+
async function engineOf(kind) {
|
|
67
|
+
if (kind === "oniguruma") {
|
|
68
|
+
const { createOnigurumaEngine } = await import("shiki/engine/oniguruma");
|
|
69
|
+
return createOnigurumaEngine(import("shiki/wasm"));
|
|
70
|
+
}
|
|
71
|
+
const { createJavaScriptRegexEngine } = await import("shiki/engine/javascript");
|
|
72
|
+
return createJavaScriptRegexEngine();
|
|
73
|
+
}
|
|
74
|
+
function tokenizeLine(grammar, model, line, state) {
|
|
75
|
+
if (line.length >= 2e4) return {
|
|
76
|
+
tokens: Uint32Array.of(line.length, defaultMetadata(model.languageId)),
|
|
77
|
+
endState: state
|
|
78
|
+
};
|
|
79
|
+
const result = grammar.tokenizeLine2(line, state, 500);
|
|
80
|
+
return {
|
|
81
|
+
tokens: withEndOffsets(toMonacoTokens(result.tokens, model.languageId, model.colorIds), line.length),
|
|
82
|
+
endState: result.stoppedEarly ? state : result.ruleStack
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
var Batch = class {
|
|
86
|
+
lines = [];
|
|
87
|
+
lengths = [];
|
|
88
|
+
parts = [];
|
|
89
|
+
states = [];
|
|
90
|
+
last;
|
|
91
|
+
get empty() {
|
|
92
|
+
return this.lines.length === 0;
|
|
93
|
+
}
|
|
94
|
+
add(line, tokens, state) {
|
|
95
|
+
const previous = this.last?.line === line - 1 ? this.last.state : null;
|
|
96
|
+
this.lines.push(line + 1);
|
|
97
|
+
this.lengths.push(tokens.length);
|
|
98
|
+
this.parts.push(tokens);
|
|
99
|
+
this.states.push(stateDiff(previous, state));
|
|
100
|
+
this.last = {
|
|
101
|
+
line,
|
|
102
|
+
state
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
post(post, model, version) {
|
|
106
|
+
const tokens = new Uint32Array(this.lengths.reduce((sum, length) => sum + length, 0));
|
|
107
|
+
let offset = 0;
|
|
108
|
+
for (const part of this.parts) {
|
|
109
|
+
tokens.set(part, offset);
|
|
110
|
+
offset += part.length;
|
|
111
|
+
}
|
|
112
|
+
const lines = Uint32Array.from(this.lines);
|
|
113
|
+
const lengths = Uint32Array.from(this.lengths);
|
|
114
|
+
post({
|
|
115
|
+
kind: "tokens",
|
|
116
|
+
model,
|
|
117
|
+
version,
|
|
118
|
+
lines,
|
|
119
|
+
lengths,
|
|
120
|
+
tokens,
|
|
121
|
+
states: this.states
|
|
122
|
+
}, [
|
|
123
|
+
tokens.buffer,
|
|
124
|
+
lines.buffer,
|
|
125
|
+
lengths.buffer
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var TokenizationWorker = class {
|
|
130
|
+
send;
|
|
131
|
+
schedule;
|
|
132
|
+
now;
|
|
133
|
+
highlighter;
|
|
134
|
+
models = /* @__PURE__ */ new Map();
|
|
135
|
+
scheduled = false;
|
|
136
|
+
failed = false;
|
|
137
|
+
constructor(send, schedule = (work) => {
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
work();
|
|
140
|
+
}, 0);
|
|
141
|
+
}, now = () => performance.now()) {
|
|
142
|
+
this.send = send;
|
|
143
|
+
this.schedule = schedule;
|
|
144
|
+
this.now = now;
|
|
145
|
+
}
|
|
146
|
+
wake() {
|
|
147
|
+
if (this.scheduled || this.failed || !this.highlighter || this.models.values().every((model) => !hasWork(model))) return;
|
|
148
|
+
this.scheduled = true;
|
|
149
|
+
this.schedule(() => this.work());
|
|
150
|
+
}
|
|
151
|
+
async work() {
|
|
152
|
+
try {
|
|
153
|
+
const highlighter = await this.highlighter;
|
|
154
|
+
if (highlighter) {
|
|
155
|
+
const deadline = this.now() + 8;
|
|
156
|
+
const turns = this.models.values().toArray();
|
|
157
|
+
for (const model of turns) {
|
|
158
|
+
if (this.now() >= deadline) break;
|
|
159
|
+
if (hasWork(model)) {
|
|
160
|
+
this.tokenizeSlice(highlighter, model, deadline);
|
|
161
|
+
if (this.models.delete(model.id)) this.models.set(model.id, model);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.failed = true;
|
|
167
|
+
this.send({
|
|
168
|
+
kind: "failed",
|
|
169
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
170
|
+
}, []);
|
|
171
|
+
} finally {
|
|
172
|
+
this.scheduled = false;
|
|
173
|
+
}
|
|
174
|
+
this.wake();
|
|
175
|
+
}
|
|
176
|
+
tokenizeSlice(highlighter, model, deadline) {
|
|
177
|
+
let grammar;
|
|
178
|
+
try {
|
|
179
|
+
highlighter.setTheme(model.theme);
|
|
180
|
+
grammar = highlighter.getLanguage(model.language);
|
|
181
|
+
} catch {
|
|
182
|
+
this.models.delete(model.id);
|
|
183
|
+
this.send({
|
|
184
|
+
kind: "unavailable",
|
|
185
|
+
model: model.id
|
|
186
|
+
}, []);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const batch = new Batch();
|
|
190
|
+
const requested = [...model.requested];
|
|
191
|
+
model.requested.length = 0;
|
|
192
|
+
for (const { start, end } of requested) for (let line = start; line < Math.min(end, model.lines.length); line += 1) {
|
|
193
|
+
const tokens = model.tokens[line];
|
|
194
|
+
const state = model.states[line];
|
|
195
|
+
if (tokens && state && !model.invalid.has(line)) batch.add(line, tokens, state);
|
|
196
|
+
}
|
|
197
|
+
while (this.now() < deadline) {
|
|
198
|
+
const line = model.invalid.first();
|
|
199
|
+
if (line === void 0) break;
|
|
200
|
+
const start = line === 0 ? INITIAL : model.states[line - 1] ?? INITIAL;
|
|
201
|
+
const { tokens, endState } = tokenizeLine(grammar, model, model.lines[line] ?? "", start);
|
|
202
|
+
const previous = model.states[line];
|
|
203
|
+
model.states[line] = endState;
|
|
204
|
+
model.tokens[line] = tokens;
|
|
205
|
+
model.invalid.delete(line);
|
|
206
|
+
if (line + 1 < model.lines.length && !previous?.equals(endState)) model.invalid.add(line + 1, line + 2);
|
|
207
|
+
batch.add(line, tokens, endState);
|
|
208
|
+
}
|
|
209
|
+
if (!batch.empty) batch.post(this.send, model.id, model.version);
|
|
210
|
+
if (model.invalid.first() === void 0 && !model.finished) {
|
|
211
|
+
model.finished = true;
|
|
212
|
+
this.send({
|
|
213
|
+
kind: "finished",
|
|
214
|
+
model: model.id,
|
|
215
|
+
version: model.version
|
|
216
|
+
}, []);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
receive(message) {
|
|
220
|
+
switch (message.kind) {
|
|
221
|
+
case "build":
|
|
222
|
+
this.highlighter = (async () => createHighlighterCore({
|
|
223
|
+
themes: [...message.themes],
|
|
224
|
+
langs: [...message.languages],
|
|
225
|
+
engine: await engineOf(message.engine)
|
|
226
|
+
}))();
|
|
227
|
+
break;
|
|
228
|
+
case "languages":
|
|
229
|
+
this.highlighter = this.highlighter?.then((highlighter) => {
|
|
230
|
+
try {
|
|
231
|
+
highlighter.loadLanguageSync([...message.languages]);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.warn("inkstone: the tokenization worker could not load a grammar", error);
|
|
234
|
+
}
|
|
235
|
+
return highlighter;
|
|
236
|
+
});
|
|
237
|
+
break;
|
|
238
|
+
case "open":
|
|
239
|
+
this.models.set(message.model, {
|
|
240
|
+
id: message.model,
|
|
241
|
+
language: message.language,
|
|
242
|
+
languageId: message.languageId,
|
|
243
|
+
theme: message.theme,
|
|
244
|
+
colorIds: message.colorIds,
|
|
245
|
+
version: message.version,
|
|
246
|
+
lines: [...message.lines],
|
|
247
|
+
states: [],
|
|
248
|
+
tokens: [],
|
|
249
|
+
invalid: new InvalidLines(message.lines.length),
|
|
250
|
+
requested: [],
|
|
251
|
+
finished: false
|
|
252
|
+
});
|
|
253
|
+
break;
|
|
254
|
+
case "change": {
|
|
255
|
+
const model = this.models.get(message.model);
|
|
256
|
+
if (model) {
|
|
257
|
+
for (const change of message.changes) {
|
|
258
|
+
const splice = lineSplice(change);
|
|
259
|
+
applyChange(model.lines, change);
|
|
260
|
+
applySplice(model.states, splice, () => void 0);
|
|
261
|
+
applySplice(model.tokens, splice, () => void 0);
|
|
262
|
+
model.invalid.splice(splice);
|
|
263
|
+
}
|
|
264
|
+
model.version = message.version;
|
|
265
|
+
model.finished = false;
|
|
266
|
+
model.requested.length = 0;
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case "retokenize":
|
|
271
|
+
this.models.get(message.model)?.requested.push({
|
|
272
|
+
start: message.start - 1,
|
|
273
|
+
end: message.end - 1
|
|
274
|
+
});
|
|
275
|
+
break;
|
|
276
|
+
case "close": this.models.delete(message.model);
|
|
277
|
+
}
|
|
278
|
+
this.wake();
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/tokenization/worker-entry.ts
|
|
283
|
+
function startTokenizationWorker() {
|
|
284
|
+
const scope = globalThis;
|
|
285
|
+
const worker = new TokenizationWorker((message, transfer) => {
|
|
286
|
+
scope.postMessage(message, transfer);
|
|
287
|
+
});
|
|
288
|
+
scope.addEventListener("message", (event) => {
|
|
289
|
+
worker.receive(event.data);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/workers/textmate.worker.js
|
|
294
|
+
startTokenizationWorker();
|
|
295
|
+
//#endregion
|
|
296
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coldsmirk/inkstone-monaco",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Framework-agnostic Monaco assembly: lazy offline host (all language workers bundled, no CDN), opt-in Simplified-Chinese UI via monaco's official NLS catalog, a streaming editor controller for AI-typed content, and the shared Shiki highlighting bridge.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"monaco",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@shikijs/vscode-textmate": "^10.0.2",
|
|
43
|
-
"
|
|
43
|
+
"shiki": "^4.4.3",
|
|
44
|
+
"@coldsmirk/inkstone-core": "^0.30.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"monaco-editor": "^0.56.0",
|