@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 +37 -0
- package/dist/index.cjs +135 -0
- package/dist/index.d.cts +113 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +133 -0
- package/dist/workers/css.worker.js +3 -0
- package/dist/workers/editor.worker.js +5 -0
- package/dist/workers/html.worker.js +3 -0
- package/dist/workers/json.worker.js +2 -0
- package/dist/workers/ts.worker.js +3 -0
- package/package.json +62 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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,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";
|
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
|
+
}
|