@coldsmirk/inkstone-monaco 0.8.2

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 ADDED
@@ -0,0 +1,37 @@
1
+ # @coldsmirk/inkstone-monaco
2
+
3
+ Framework-agnostic [Monaco](https://microsoft.github.io/monaco-editor/) assembly: a lazy **offline host** (bundled workers, no CDN loader), opt-in Simplified-Chinese UI via the official NLS catalog, embedded-friendly geometry fixes, and a streaming controller for AI-typed content.
4
+
5
+ Part of [inkstone](https://github.com/coldsmirk/inkstone). For a drop-in React component use [`@coldsmirk/inkstone-react`](https://www.npmjs.com/package/@coldsmirk/inkstone-react)'s `<MonacoEditor>`, which is built on this package; reach for this layer to wire the host into your own components (any framework), or to gate raw `@monaco-editor/react` components (e.g. `<DiffEditor>`) on the same offline module.
6
+
7
+ ## What you get
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. Idempotent — every caller gets the same module; a load failure is not cached, so a fresh call retries.
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
+ - **Embedded-editor fix** — 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`.
12
+ - **`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, `isStreaming()` for the onChange guard, and `replace(old, new, all)` for exact edits that preserve undo/cursor/folds.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pnpm add @coldsmirk/inkstone-monaco monaco-editor
18
+ ```
19
+
20
+ `monaco-editor` is a **peer dependency** — your app owns the version. Node.js >= 22 for build / SSR hosts; workers are bundled by your bundler from `new Worker(new URL(...))`, which any Vite / Rollup / webpack 5 setup handles natively.
21
+
22
+ ## Quick start
23
+
24
+ ```ts
25
+ import { ensureMonacoHost } from "@coldsmirk/inkstone-monaco";
26
+
27
+ const monaco = await ensureMonacoHost({ locale: "zh-cn" });
28
+ const editor = monaco.editor.create(container, { value: "const x = 1;\n", language: "javascript" });
29
+ ```
30
+
31
+ ## The one Monaco rule
32
+
33
+ The host evaluates the requested locale catalog **before** the monaco module, because Monaco resolves its UI strings at module-evaluation time. A static `import "monaco-editor"` anywhere in your app would evaluate first and lock the UI to English — so never import it statically; take the instance from `ensureMonacoHost()`. **Type-only imports are fine** (`import type { editor } from "monaco-editor"` erases at compile time).
34
+
35
+ ## License
36
+
37
+ UNLICENSED — proprietary. All rights reserved; no use, copying, or redistribution without the author's permission.
package/dist/index.cjs ADDED
@@ -0,0 +1,135 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/host.ts
3
+ let hostPromise = null;
4
+ function editorWorker() {
5
+ return new Worker(new URL("workers/editor.worker.js", require("url").pathToFileURL(__filename).href), { type: "module" });
6
+ }
7
+ const WORKER_BY_LABEL = {
8
+ typescript: () => new Worker(new URL("workers/ts.worker.js", require("url").pathToFileURL(__filename).href), { type: "module" }),
9
+ json: () => new Worker(new URL("workers/json.worker.js", require("url").pathToFileURL(__filename).href), { type: "module" }),
10
+ css: () => new Worker(new URL("workers/css.worker.js", require("url").pathToFileURL(__filename).href), { type: "module" }),
11
+ html: () => new Worker(new URL("workers/html.worker.js", require("url").pathToFileURL(__filename).href), { type: "module" })
12
+ };
13
+ const WORKER_ALIASES = {
14
+ javascript: "typescript",
15
+ scss: "css",
16
+ less: "css",
17
+ handlebars: "html",
18
+ razor: "html"
19
+ };
20
+ function builtinGetWorker(label) {
21
+ const factory = WORKER_BY_LABEL[WORKER_ALIASES[label] ?? label];
22
+ return factory ? factory() : editorWorker();
23
+ }
24
+ const HOVER_GEOMETRY_STYLE_SELECTOR = "style[data-inkstone=\"monaco-hover-geometry\"]";
25
+ function installHoverGeometryFix() {
26
+ if (document.querySelector(HOVER_GEOMETRY_STYLE_SELECTOR)) return;
27
+ const style = document.createElement("style");
28
+ style.dataset.inkstone = "monaco-hover-geometry";
29
+ style.textContent = ".context-view:has(> .workbench-hover-container) { width: max-content !important; }";
30
+ document.head.append(style);
31
+ }
32
+ function ensureMonacoHost(options = {}) {
33
+ hostPromise ??= (async () => {
34
+ const { locale = "en", getWorker } = options;
35
+ if (locale === "zh-cn") await import("monaco-editor/esm/nls.messages.zh-cn.js");
36
+ const monaco = await import("monaco-editor");
37
+ globalThis.MonacoEnvironment = { getWorker: (workerId, label) => getWorker?.(workerId, label) ?? builtinGetWorker(label) };
38
+ installHoverGeometryFix();
39
+ return monaco;
40
+ })().catch((error) => {
41
+ hostPromise = null;
42
+ throw error;
43
+ });
44
+ return hostPromise;
45
+ }
46
+ //#endregion
47
+ //#region src/streaming.ts
48
+ var StreamingEditorController = class {
49
+ editor = null;
50
+ writing = false;
51
+ buffer = "";
52
+ attach(instance) {
53
+ this.editor = instance;
54
+ if (this.writing) {
55
+ instance.updateOptions({ readOnly: true });
56
+ instance.setValue(this.buffer);
57
+ instance.revealLine(instance.getModel()?.getLineCount() ?? 1);
58
+ }
59
+ }
60
+ detach(instance) {
61
+ if (this.editor === instance) this.editor = null;
62
+ }
63
+ isStreaming() {
64
+ return this.writing;
65
+ }
66
+ begin() {
67
+ this.writing = true;
68
+ this.buffer = "";
69
+ if (this.editor) {
70
+ this.editor.updateOptions({ readOnly: true });
71
+ this.editor.setValue("");
72
+ }
73
+ }
74
+ append(text) {
75
+ if (!this.writing) return;
76
+ this.buffer += text;
77
+ const model = this.editor?.getModel();
78
+ if (!this.editor || !model) return;
79
+ const line = model.getLineCount();
80
+ const column = model.getLineMaxColumn(line);
81
+ model.applyEdits([{
82
+ range: {
83
+ startLineNumber: line,
84
+ startColumn: column,
85
+ endLineNumber: line,
86
+ endColumn: column
87
+ },
88
+ text
89
+ }]);
90
+ this.editor.revealLine(model.getLineCount());
91
+ }
92
+ end() {
93
+ this.writing = false;
94
+ this.editor?.updateOptions({ readOnly: false });
95
+ return this.buffer;
96
+ }
97
+ discard() {
98
+ this.writing = false;
99
+ this.buffer = "";
100
+ this.editor?.updateOptions({ readOnly: false });
101
+ }
102
+ replace(search, replaceWith, all) {
103
+ const model = this.editor?.getModel();
104
+ if (search === "" || !this.editor || !model) return false;
105
+ const source = model.getValue();
106
+ const offsets = [];
107
+ let from = 0;
108
+ for (;;) {
109
+ const index = source.indexOf(search, from);
110
+ if (index === -1) break;
111
+ offsets.push(index);
112
+ if (!all) break;
113
+ from = index + search.length;
114
+ }
115
+ if (offsets.length === 0) return false;
116
+ const edits = offsets.map((offset) => {
117
+ const start = model.getPositionAt(offset);
118
+ const end = model.getPositionAt(offset + search.length);
119
+ return {
120
+ range: {
121
+ startLineNumber: start.lineNumber,
122
+ startColumn: start.column,
123
+ endLineNumber: end.lineNumber,
124
+ endColumn: end.column
125
+ },
126
+ text: replaceWith
127
+ };
128
+ });
129
+ this.editor.executeEdits("inkstone-stream-replace", edits);
130
+ return true;
131
+ }
132
+ };
133
+ //#endregion
134
+ exports.StreamingEditorController = StreamingEditorController;
135
+ exports.ensureMonacoHost = ensureMonacoHost;
@@ -0,0 +1,113 @@
1
+ import * as monacoEditor from "monaco-editor";
2
+ import { editor } from "monaco-editor";
3
+
4
+ //#region src/host.d.ts
5
+ /**
6
+ * Supported UI locales for Monaco's built-in chrome (context menu, find widget, command
7
+ * palette …). `"en"` is Monaco's built-in default; `"zh-cn"` loads the official
8
+ * Simplified-Chinese catalog that monaco-editor ships in its ESM distribution.
9
+ */
10
+ type MonacoUiLocale = "en" | "zh-cn";
11
+ interface MonacoHostOptions {
12
+ /**
13
+ * UI locale for Monaco's built-in chrome. A non-`"en"` locale is applied by evaluating the
14
+ * official locale catalog before the monaco module itself — which is why the host owns the
15
+ * monaco import: a static `import "monaco-editor"` elsewhere would evaluate first and lock
16
+ * the UI to English.
17
+ *
18
+ * @default "en"
19
+ */
20
+ locale?: MonacoUiLocale;
21
+ /**
22
+ * Custom worker routing, tried first; return `undefined` to fall back to the built-in
23
+ * routing. Monaco's built-in languages are fully covered without this — it exists for
24
+ * workers this package does not ship (a third-party language server, e.g. monaco-yaml):
25
+ * key on `label` and return `new Worker(new URL(...))` for the ones you handle, `undefined`
26
+ * otherwise. See the README's "Language workers" section for a full example.
27
+ */
28
+ getWorker?: (workerId: string, label: string) => Worker | undefined;
29
+ }
30
+ /**
31
+ * The monaco module namespace, as resolved by {@link ensureMonacoHost}.
32
+ */
33
+ type MonacoModule = typeof monacoEditor;
34
+ declare function ensureMonacoHost(options?: MonacoHostOptions): Promise<MonacoModule>;
35
+ //#endregion
36
+ //#region src/languages.d.ts
37
+ /**
38
+ * Monaco's built-in language ids — the set Monaco ships tokenizers for out of the box, used
39
+ * to type `<MonacoEditor language>`. Monaco also accepts any custom-registered id (e.g. a
40
+ * language wired through `getWorker` such as monaco-yaml), so the type stays open: the union
41
+ * drives editor autocomplete while `string & Record<never, never>` still admits any string.
42
+ */
43
+ type MonacoLanguage = "abap" | "apex" | "azcli" | "bat" | "bicep" | "cameligo" | "clojure" | "coffeescript" | "cpp" | "csharp" | "csp" | "css" | "cypher" | "dart" | "dockerfile" | "ecl" | "elixir" | "flow9" | "freemarker2" | "fsharp" | "go" | "graphql" | "handlebars" | "hcl" | "html" | "ini" | "java" | "javascript" | "json" | "julia" | "kotlin" | "less" | "lexon" | "liquid" | "lua" | "m3" | "markdown" | "mdx" | "mips" | "msdax" | "mysql" | "objective-c" | "pascal" | "pascaligo" | "perl" | "pgsql" | "php" | "pla" | "postiats" | "powerquery" | "powershell" | "protobuf" | "pug" | "python" | "qsharp" | "r" | "razor" | "redis" | "redshift" | "restructuredtext" | "ruby" | "rust" | "sb" | "scala" | "scheme" | "scss" | "shell" | "solidity" | "sophia" | "sparql" | "sql" | "st" | "swift" | "systemverilog" | "tcl" | "twig" | "typescript" | "typespec" | "vb" | "wgsl" | "xml" | "yaml" | "plaintext" | (string & Record<never, never>);
44
+ //#endregion
45
+ //#region src/streaming.d.ts
46
+ /**
47
+ * The editor surface the controller drives — satisfied by any Monaco code editor
48
+ * (type-only import, so this package never evaluates the monaco module itself).
49
+ */
50
+ type StreamTargetEditor = editor.ICodeEditor;
51
+ /**
52
+ * Streams generated text (an AI response, a log tail) into a Monaco editor chunk by chunk,
53
+ * decoupled from the editor's controlled-value cycle.
54
+ *
55
+ * Why not just write the store on every chunk: the editor is typically a controlled
56
+ * component, so routing every token through app state re-renders the tree per token — and
57
+ * the controlled value would clobber the half-written document. Instead the controller
58
+ * mutates the Monaco model directly (append-at-EOF via `applyEdits`, scrolled into view),
59
+ * the editor's `onChange` short-circuits on {@link isStreaming}, and the caller reconciles
60
+ * app state once from {@link end}'s return value.
61
+ *
62
+ * The target editor may mount late or unmount mid-stream (keep-alive tab hosts): chunks
63
+ * accumulate in a buffer that is replayed on {@link attach}, so the final document is
64
+ * complete even if the editor never mounts.
65
+ */
66
+ declare class StreamingEditorController {
67
+ private editor;
68
+ private writing;
69
+ private buffer;
70
+ /**
71
+ * Bind the mounted editor. If a stream is in progress, the buffered content is synced
72
+ * into the model immediately and the editor is set read-only.
73
+ */
74
+ attach(instance: StreamTargetEditor): void;
75
+ /**
76
+ * Release the editor (unmount). A stream in progress keeps accumulating in the buffer.
77
+ */
78
+ detach(instance: StreamTargetEditor): void;
79
+ /**
80
+ * Whether a stream is in progress — the editor's `onChange` must not write back to app
81
+ * state while this is true.
82
+ */
83
+ isStreaming(): boolean;
84
+ /**
85
+ * Start a stream: clears the buffer and the editor, and sets it read-only.
86
+ */
87
+ begin(): void;
88
+ /**
89
+ * Append a chunk: buffered always, and applied at end-of-document when an editor is
90
+ * attached (a minimal edit, scrolled into view).
91
+ */
92
+ append(text: string): void;
93
+ /**
94
+ * End the stream and return the full streamed text — the caller reconciles it into app
95
+ * state (one write, replacing the per-token churn).
96
+ */
97
+ end(): string;
98
+ /**
99
+ * Abort mid-stream WITHOUT yielding the buffer: clears the writing/read-only state and
100
+ * drops the partial text. For teardown paths (document switch / reset) where flushing
101
+ * the partial stream into the now-current document would corrupt it.
102
+ */
103
+ discard(): void;
104
+ /**
105
+ * Exact find-and-replace on the attached editor, located by character offset and applied
106
+ * via `executeEdits` — preserving the undo stack, cursor, and fold state. Returns whether
107
+ * it was applied (`false` = no editor / empty search / no match; the caller falls back to
108
+ * a state write).
109
+ */
110
+ replace(search: string, replaceWith: string, all: boolean): boolean;
111
+ }
112
+ //#endregion
113
+ export { type MonacoHostOptions, type MonacoLanguage, type MonacoModule, type MonacoUiLocale, type StreamTargetEditor, StreamingEditorController, ensureMonacoHost };
@@ -0,0 +1,113 @@
1
+ import * as monacoEditor from "monaco-editor";
2
+ import { editor } from "monaco-editor";
3
+
4
+ //#region src/host.d.ts
5
+ /**
6
+ * Supported UI locales for Monaco's built-in chrome (context menu, find widget, command
7
+ * palette …). `"en"` is Monaco's built-in default; `"zh-cn"` loads the official
8
+ * Simplified-Chinese catalog that monaco-editor ships in its ESM distribution.
9
+ */
10
+ type MonacoUiLocale = "en" | "zh-cn";
11
+ interface MonacoHostOptions {
12
+ /**
13
+ * UI locale for Monaco's built-in chrome. A non-`"en"` locale is applied by evaluating the
14
+ * official locale catalog before the monaco module itself — which is why the host owns the
15
+ * monaco import: a static `import "monaco-editor"` elsewhere would evaluate first and lock
16
+ * the UI to English.
17
+ *
18
+ * @default "en"
19
+ */
20
+ locale?: MonacoUiLocale;
21
+ /**
22
+ * Custom worker routing, tried first; return `undefined` to fall back to the built-in
23
+ * routing. Monaco's built-in languages are fully covered without this — it exists for
24
+ * workers this package does not ship (a third-party language server, e.g. monaco-yaml):
25
+ * key on `label` and return `new Worker(new URL(...))` for the ones you handle, `undefined`
26
+ * otherwise. See the README's "Language workers" section for a full example.
27
+ */
28
+ getWorker?: (workerId: string, label: string) => Worker | undefined;
29
+ }
30
+ /**
31
+ * The monaco module namespace, as resolved by {@link ensureMonacoHost}.
32
+ */
33
+ type MonacoModule = typeof monacoEditor;
34
+ declare function ensureMonacoHost(options?: MonacoHostOptions): Promise<MonacoModule>;
35
+ //#endregion
36
+ //#region src/languages.d.ts
37
+ /**
38
+ * Monaco's built-in language ids — the set Monaco ships tokenizers for out of the box, used
39
+ * to type `<MonacoEditor language>`. Monaco also accepts any custom-registered id (e.g. a
40
+ * language wired through `getWorker` such as monaco-yaml), so the type stays open: the union
41
+ * drives editor autocomplete while `string & Record<never, never>` still admits any string.
42
+ */
43
+ type MonacoLanguage = "abap" | "apex" | "azcli" | "bat" | "bicep" | "cameligo" | "clojure" | "coffeescript" | "cpp" | "csharp" | "csp" | "css" | "cypher" | "dart" | "dockerfile" | "ecl" | "elixir" | "flow9" | "freemarker2" | "fsharp" | "go" | "graphql" | "handlebars" | "hcl" | "html" | "ini" | "java" | "javascript" | "json" | "julia" | "kotlin" | "less" | "lexon" | "liquid" | "lua" | "m3" | "markdown" | "mdx" | "mips" | "msdax" | "mysql" | "objective-c" | "pascal" | "pascaligo" | "perl" | "pgsql" | "php" | "pla" | "postiats" | "powerquery" | "powershell" | "protobuf" | "pug" | "python" | "qsharp" | "r" | "razor" | "redis" | "redshift" | "restructuredtext" | "ruby" | "rust" | "sb" | "scala" | "scheme" | "scss" | "shell" | "solidity" | "sophia" | "sparql" | "sql" | "st" | "swift" | "systemverilog" | "tcl" | "twig" | "typescript" | "typespec" | "vb" | "wgsl" | "xml" | "yaml" | "plaintext" | (string & Record<never, never>);
44
+ //#endregion
45
+ //#region src/streaming.d.ts
46
+ /**
47
+ * The editor surface the controller drives — satisfied by any Monaco code editor
48
+ * (type-only import, so this package never evaluates the monaco module itself).
49
+ */
50
+ type StreamTargetEditor = editor.ICodeEditor;
51
+ /**
52
+ * Streams generated text (an AI response, a log tail) into a Monaco editor chunk by chunk,
53
+ * decoupled from the editor's controlled-value cycle.
54
+ *
55
+ * Why not just write the store on every chunk: the editor is typically a controlled
56
+ * component, so routing every token through app state re-renders the tree per token — and
57
+ * the controlled value would clobber the half-written document. Instead the controller
58
+ * mutates the Monaco model directly (append-at-EOF via `applyEdits`, scrolled into view),
59
+ * the editor's `onChange` short-circuits on {@link isStreaming}, and the caller reconciles
60
+ * app state once from {@link end}'s return value.
61
+ *
62
+ * The target editor may mount late or unmount mid-stream (keep-alive tab hosts): chunks
63
+ * accumulate in a buffer that is replayed on {@link attach}, so the final document is
64
+ * complete even if the editor never mounts.
65
+ */
66
+ declare class StreamingEditorController {
67
+ private editor;
68
+ private writing;
69
+ private buffer;
70
+ /**
71
+ * Bind the mounted editor. If a stream is in progress, the buffered content is synced
72
+ * into the model immediately and the editor is set read-only.
73
+ */
74
+ attach(instance: StreamTargetEditor): void;
75
+ /**
76
+ * Release the editor (unmount). A stream in progress keeps accumulating in the buffer.
77
+ */
78
+ detach(instance: StreamTargetEditor): void;
79
+ /**
80
+ * Whether a stream is in progress — the editor's `onChange` must not write back to app
81
+ * state while this is true.
82
+ */
83
+ isStreaming(): boolean;
84
+ /**
85
+ * Start a stream: clears the buffer and the editor, and sets it read-only.
86
+ */
87
+ begin(): void;
88
+ /**
89
+ * Append a chunk: buffered always, and applied at end-of-document when an editor is
90
+ * attached (a minimal edit, scrolled into view).
91
+ */
92
+ append(text: string): void;
93
+ /**
94
+ * End the stream and return the full streamed text — the caller reconciles it into app
95
+ * state (one write, replacing the per-token churn).
96
+ */
97
+ end(): string;
98
+ /**
99
+ * Abort mid-stream WITHOUT yielding the buffer: clears the writing/read-only state and
100
+ * drops the partial text. For teardown paths (document switch / reset) where flushing
101
+ * the partial stream into the now-current document would corrupt it.
102
+ */
103
+ discard(): void;
104
+ /**
105
+ * Exact find-and-replace on the attached editor, located by character offset and applied
106
+ * via `executeEdits` — preserving the undo stack, cursor, and fold state. Returns whether
107
+ * it was applied (`false` = no editor / empty search / no match; the caller falls back to
108
+ * a state write).
109
+ */
110
+ replace(search: string, replaceWith: string, all: boolean): boolean;
111
+ }
112
+ //#endregion
113
+ export { type MonacoHostOptions, type MonacoLanguage, type MonacoModule, type MonacoUiLocale, type StreamTargetEditor, StreamingEditorController, ensureMonacoHost };
package/dist/index.js ADDED
@@ -0,0 +1,133 @@
1
+ //#region src/host.ts
2
+ let hostPromise = null;
3
+ function editorWorker() {
4
+ return new Worker(new URL("workers/editor.worker.js", import.meta.url), { type: "module" });
5
+ }
6
+ const WORKER_BY_LABEL = {
7
+ typescript: () => new Worker(new URL("workers/ts.worker.js", import.meta.url), { type: "module" }),
8
+ json: () => new Worker(new URL("workers/json.worker.js", import.meta.url), { type: "module" }),
9
+ css: () => new Worker(new URL("workers/css.worker.js", import.meta.url), { type: "module" }),
10
+ html: () => new Worker(new URL("workers/html.worker.js", import.meta.url), { type: "module" })
11
+ };
12
+ const WORKER_ALIASES = {
13
+ javascript: "typescript",
14
+ scss: "css",
15
+ less: "css",
16
+ handlebars: "html",
17
+ razor: "html"
18
+ };
19
+ function builtinGetWorker(label) {
20
+ const factory = WORKER_BY_LABEL[WORKER_ALIASES[label] ?? label];
21
+ return factory ? factory() : editorWorker();
22
+ }
23
+ const HOVER_GEOMETRY_STYLE_SELECTOR = "style[data-inkstone=\"monaco-hover-geometry\"]";
24
+ function installHoverGeometryFix() {
25
+ if (document.querySelector(HOVER_GEOMETRY_STYLE_SELECTOR)) return;
26
+ const style = document.createElement("style");
27
+ style.dataset.inkstone = "monaco-hover-geometry";
28
+ style.textContent = ".context-view:has(> .workbench-hover-container) { width: max-content !important; }";
29
+ document.head.append(style);
30
+ }
31
+ function ensureMonacoHost(options = {}) {
32
+ hostPromise ??= (async () => {
33
+ const { locale = "en", getWorker } = options;
34
+ if (locale === "zh-cn") await import("monaco-editor/esm/nls.messages.zh-cn.js");
35
+ const monaco = await import("monaco-editor");
36
+ globalThis.MonacoEnvironment = { getWorker: (workerId, label) => getWorker?.(workerId, label) ?? builtinGetWorker(label) };
37
+ installHoverGeometryFix();
38
+ return monaco;
39
+ })().catch((error) => {
40
+ hostPromise = null;
41
+ throw error;
42
+ });
43
+ return hostPromise;
44
+ }
45
+ //#endregion
46
+ //#region src/streaming.ts
47
+ var StreamingEditorController = class {
48
+ editor = null;
49
+ writing = false;
50
+ buffer = "";
51
+ attach(instance) {
52
+ this.editor = instance;
53
+ if (this.writing) {
54
+ instance.updateOptions({ readOnly: true });
55
+ instance.setValue(this.buffer);
56
+ instance.revealLine(instance.getModel()?.getLineCount() ?? 1);
57
+ }
58
+ }
59
+ detach(instance) {
60
+ if (this.editor === instance) this.editor = null;
61
+ }
62
+ isStreaming() {
63
+ return this.writing;
64
+ }
65
+ begin() {
66
+ this.writing = true;
67
+ this.buffer = "";
68
+ if (this.editor) {
69
+ this.editor.updateOptions({ readOnly: true });
70
+ this.editor.setValue("");
71
+ }
72
+ }
73
+ append(text) {
74
+ if (!this.writing) return;
75
+ this.buffer += text;
76
+ const model = this.editor?.getModel();
77
+ if (!this.editor || !model) return;
78
+ const line = model.getLineCount();
79
+ const column = model.getLineMaxColumn(line);
80
+ model.applyEdits([{
81
+ range: {
82
+ startLineNumber: line,
83
+ startColumn: column,
84
+ endLineNumber: line,
85
+ endColumn: column
86
+ },
87
+ text
88
+ }]);
89
+ this.editor.revealLine(model.getLineCount());
90
+ }
91
+ end() {
92
+ this.writing = false;
93
+ this.editor?.updateOptions({ readOnly: false });
94
+ return this.buffer;
95
+ }
96
+ discard() {
97
+ this.writing = false;
98
+ this.buffer = "";
99
+ this.editor?.updateOptions({ readOnly: false });
100
+ }
101
+ replace(search, replaceWith, all) {
102
+ const model = this.editor?.getModel();
103
+ if (search === "" || !this.editor || !model) return false;
104
+ const source = model.getValue();
105
+ const offsets = [];
106
+ let from = 0;
107
+ for (;;) {
108
+ const index = source.indexOf(search, from);
109
+ if (index === -1) break;
110
+ offsets.push(index);
111
+ if (!all) break;
112
+ from = index + search.length;
113
+ }
114
+ if (offsets.length === 0) return false;
115
+ const edits = offsets.map((offset) => {
116
+ const start = model.getPositionAt(offset);
117
+ const end = model.getPositionAt(offset + search.length);
118
+ return {
119
+ range: {
120
+ startLineNumber: start.lineNumber,
121
+ startColumn: start.column,
122
+ endLineNumber: end.lineNumber,
123
+ endColumn: end.column
124
+ },
125
+ text: replaceWith
126
+ };
127
+ });
128
+ this.editor.executeEdits("inkstone-stream-replace", edits);
129
+ return true;
130
+ }
131
+ };
132
+ //#endregion
133
+ export { StreamingEditorController, ensureMonacoHost };
@@ -0,0 +1,3 @@
1
+ // Worker entry: the CSS language worker (also serves scss/less). See editor.worker.js for
2
+ // why this indirection exists.
3
+ import "monaco-editor/esm/vs/language/css/css.worker.js";
@@ -0,0 +1,5 @@
1
+ // Worker entry: re-exports monaco's base editor worker through a file that lives inside
2
+ // this package, so the host can reference it with `new URL("./workers/…", import.meta.url)`
3
+ // — the only worker form bundlers resolve inside a published dependency (Vite's `?worker`
4
+ // suffix fails to load from node_modules).
5
+ import "monaco-editor/esm/vs/editor/editor.worker.js";
@@ -0,0 +1,3 @@
1
+ // Worker entry: the HTML language worker (also serves handlebars/razor). See editor.worker.js
2
+ // for why this indirection exists.
3
+ import "monaco-editor/esm/vs/language/html/html.worker.js";
@@ -0,0 +1,2 @@
1
+ // Worker entry: the JSON language worker. See editor.worker.js for why this indirection exists.
2
+ import "monaco-editor/esm/vs/language/json/json.worker.js";
@@ -0,0 +1,3 @@
1
+ // Worker entry: the TypeScript worker, which also serves JavaScript IntelliSense.
2
+ // See editor.worker.js for why this indirection exists.
3
+ import "monaco-editor/esm/vs/language/typescript/ts.worker.js";
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@coldsmirk/inkstone-monaco",
3
+ "version": "0.8.2",
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, and a streaming editor controller for AI-typed content.",
5
+ "keywords": [
6
+ "monaco",
7
+ "monaco-editor",
8
+ "editor",
9
+ "nls",
10
+ "zh-cn",
11
+ "worker"
12
+ ],
13
+ "homepage": "https://github.com/coldsmirk/inkstone/tree/main/packages/monaco#readme",
14
+ "bugs": "https://github.com/coldsmirk/inkstone/issues",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/coldsmirk/inkstone.git",
18
+ "directory": "packages/monaco"
19
+ },
20
+ "license": "UNLICENSED",
21
+ "author": {
22
+ "name": "Venus"
23
+ },
24
+ "sideEffects": false,
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "import": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "require": {
33
+ "types": "./dist/index.d.cts",
34
+ "default": "./dist/index.cjs"
35
+ }
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "main": "./dist/index.cjs",
40
+ "module": "./dist/index.js",
41
+ "types": "./dist/index.d.ts",
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "devDependencies": {
46
+ "monaco-editor": "^0.55.1"
47
+ },
48
+ "peerDependencies": {
49
+ "monaco-editor": "^0.55.1"
50
+ },
51
+ "engines": {
52
+ "node": ">=22"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "scripts": {
58
+ "build": "tsdown",
59
+ "clean": "rimraf dist",
60
+ "typecheck": "tsc --noEmit"
61
+ }
62
+ }