@coldsmirk/inkstone-monaco 0.8.3 → 0.9.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 +3 -1
- package/dist/index.d.ts +131 -12
- package/dist/index.js +200 -14
- package/package.json +4 -12
- package/dist/index.cjs +0 -135
- package/dist/index.d.cts +0 -113
package/README.md
CHANGED
|
@@ -8,8 +8,10 @@ Part of [inkstone](https://github.com/coldsmirk/inkstone). For a drop-in React c
|
|
|
8
8
|
|
|
9
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
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
|
+
- **`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
|
+
- **`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.
|
|
11
13
|
- **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.
|
|
14
|
+
- **`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), `isStreaming()` for the onChange guard, and `replace(old, new, all)` for exact edits that preserve undo/cursor/folds. 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.
|
|
13
15
|
|
|
14
16
|
## Install
|
|
15
17
|
|
package/dist/index.d.ts
CHANGED
|
@@ -33,14 +33,71 @@ interface MonacoHostOptions {
|
|
|
33
33
|
type MonacoModule = typeof monacoEditor;
|
|
34
34
|
declare function ensureMonacoHost(options?: MonacoHostOptions): Promise<MonacoModule>;
|
|
35
35
|
//#endregion
|
|
36
|
+
//#region src/keybindings.d.ts
|
|
37
|
+
/**
|
|
38
|
+
* A set of keybindings to swallow, plus the context key that scopes them to the editors that
|
|
39
|
+
* opted in.
|
|
40
|
+
*/
|
|
41
|
+
interface SwallowedKeybindings {
|
|
42
|
+
readonly contextKey: string;
|
|
43
|
+
readonly keybindings: readonly number[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The keybindings `disableFind` / `disableCommandPalette` swallow: the find/replace widget's
|
|
47
|
+
* entries (Ctrl/Cmd+F to find; Ctrl+H and Cmd+Alt+F, the per-platform replace bindings) and
|
|
48
|
+
* the command palette's F1 — Monaco never binds Ctrl+Shift+P (a VS Code binding). Sourced
|
|
49
|
+
* from monaco-editor 0.55.1 (`findController.js`, `standaloneCommandsQuickAccess.js`).
|
|
50
|
+
*
|
|
51
|
+
* Both replace chords are swallowed on every platform rather than branching: `CtrlCmd` already
|
|
52
|
+
* resolves per platform, and the off-platform chord (Ctrl+Alt+F on Windows, Cmd+H on macOS, where
|
|
53
|
+
* the OS takes the key before the page sees it) is one Monaco never bound.
|
|
54
|
+
*/
|
|
55
|
+
declare function disabledKeybindings(monaco: MonacoModule, disableFind: boolean, disableCommandPalette: boolean): SwallowedKeybindings[];
|
|
56
|
+
/**
|
|
57
|
+
* The slice of a standalone editor the install touches — a structural type, so the install is
|
|
58
|
+
* testable without evaluating the monaco module (which jsdom cannot do).
|
|
59
|
+
*/
|
|
60
|
+
interface KeybindingTargetEditor {
|
|
61
|
+
createContextKey: (key: string, defaultValue: boolean) => unknown;
|
|
62
|
+
addCommand: (keybinding: number, handler: () => void, context?: string) => string | null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Opt `editorInstance` into swallowing `groups` on `monaco`'s keybinding service: it always
|
|
66
|
+
* creates its own context keys (that is what scopes the rules to it), but each chord goes onto
|
|
67
|
+
* the page-global service only the first time any editor of that monaco module asks for it —
|
|
68
|
+
* monaco offers no way to unregister one.
|
|
69
|
+
*/
|
|
70
|
+
declare function installDisabledKeybindings(monaco: MonacoModule, editorInstance: KeybindingTargetEditor, groups: readonly SwallowedKeybindings[]): void;
|
|
71
|
+
//#endregion
|
|
36
72
|
//#region src/languages.d.ts
|
|
37
73
|
/**
|
|
38
|
-
* Monaco's built-in language ids
|
|
39
|
-
* to
|
|
74
|
+
* Monaco's built-in language ids: every id the editor registers out of the box, sourced from the
|
|
75
|
+
* installed monaco-editor and held to it by `languages.test.ts` — a bump that adds, renames, or
|
|
76
|
+
* drops a language fails the suite until this list is updated.
|
|
77
|
+
*
|
|
78
|
+
* These are *ids*, not display names, and the two diverge more than they look: protobuf registers
|
|
79
|
+
* as `proto`, Solidity as `sol`, Sophia as `aes`. Monaco silently falls back to `plaintext` for an
|
|
80
|
+
* unknown id, so an id that reads right but is not one of these buys no tokenizer at all.
|
|
81
|
+
*
|
|
82
|
+
* Three ids come from outside `basic-languages`: `json` (the JSON language service),
|
|
83
|
+
* `typescript` / `javascript` (also served by the TypeScript worker), and `plaintext` (the editor
|
|
84
|
+
* core's fallback language).
|
|
85
|
+
*/
|
|
86
|
+
declare const BUILT_IN_LANGUAGES: readonly ["abap", "aes", "apex", "azcli", "bat", "bicep", "c", "cameligo", "clojure", "coffeescript", "cpp", "csharp", "csp", "css", "cypher", "dart", "dockerfile", "ecl", "elixir", "flow9", "freemarker2", "freemarker2.tag-angle.interpolation-bracket", "freemarker2.tag-angle.interpolation-dollar", "freemarker2.tag-auto.interpolation-bracket", "freemarker2.tag-auto.interpolation-dollar", "freemarker2.tag-bracket.interpolation-bracket", "freemarker2.tag-bracket.interpolation-dollar", "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", "plaintext", "postiats", "powerquery", "powershell", "proto", "pug", "python", "qsharp", "r", "razor", "redis", "redshift", "restructuredtext", "ruby", "rust", "sb", "scala", "scheme", "scss", "shell", "sol", "sparql", "sql", "st", "swift", "systemverilog", "tcl", "twig", "typescript", "typespec", "vb", "verilog", "wgsl", "xml", "yaml"];
|
|
87
|
+
/**
|
|
88
|
+
* A language id Monaco registers out of the box.
|
|
89
|
+
*/
|
|
90
|
+
type MonacoBuiltInLanguage = (typeof BUILT_IN_LANGUAGES)[number];
|
|
91
|
+
/**
|
|
92
|
+
* Every built-in Monaco language id, sorted.
|
|
93
|
+
*/
|
|
94
|
+
declare const monacoLanguages: readonly MonacoBuiltInLanguage[];
|
|
95
|
+
/**
|
|
96
|
+
* The `<MonacoEditor language>` type. Monaco also accepts any custom-registered id (e.g. a
|
|
40
97
|
* language wired through `getWorker` such as monaco-yaml), so the type stays open: the union
|
|
41
98
|
* drives editor autocomplete while `string & Record<never, never>` still admits any string.
|
|
42
99
|
*/
|
|
43
|
-
type MonacoLanguage =
|
|
100
|
+
type MonacoLanguage = MonacoBuiltInLanguage | (string & Record<never, never>);
|
|
44
101
|
//#endregion
|
|
45
102
|
//#region src/streaming.d.ts
|
|
46
103
|
/**
|
|
@@ -67,13 +124,68 @@ declare class StreamingEditorController {
|
|
|
67
124
|
private editor;
|
|
68
125
|
private writing;
|
|
69
126
|
private buffer;
|
|
127
|
+
/**
|
|
128
|
+
* Every editor instance the stream locked, each with its OWN pre-lock `readOnly` setting
|
|
129
|
+
* (the stream forces `readOnly: true`, so restoring a hardcoded `false` would silently
|
|
130
|
+
* unlock an editor the app declared read-only). Keyed by the instance handle so the
|
|
131
|
+
* restore reaches detached editors too — a keep-alive host that never remounts must not
|
|
132
|
+
* stay locked forever. The watcher evicts the entry the moment the instance is disposed
|
|
133
|
+
* (an unmount tears the editor down for good): a dead editor needs no restore, and
|
|
134
|
+
* `ICodeEditor` has no `isDisposed()` to check at restore time.
|
|
135
|
+
*/
|
|
136
|
+
private readonly locked;
|
|
137
|
+
/**
|
|
138
|
+
* Every model whose content the stream overwrote, with the document it displaced. Keyed by
|
|
139
|
+
* the model handle so {@link discard} (every model) and {@link end} (every model except the
|
|
140
|
+
* one keeping the streamed text) put each document back into the model it came from — not
|
|
141
|
+
* into whichever model happens to be attached at teardown (the app may have switched
|
|
142
|
+
* documents or swapped in a replacement editor mid-stream).
|
|
143
|
+
*/
|
|
144
|
+
private readonly displaced;
|
|
145
|
+
/**
|
|
146
|
+
* The model currently holding the streamed text. {@link append} extends it chunk by chunk;
|
|
147
|
+
* any other model reaching the write path (the app swapped documents on the attached editor
|
|
148
|
+
* mid-stream) is first brought up to date with a full buffer replay — an incremental append
|
|
149
|
+
* would silently drop every chunk streamed before the swap.
|
|
150
|
+
*/
|
|
151
|
+
private synced;
|
|
152
|
+
/**
|
|
153
|
+
* Lock the editor for the stream: record its own pre-lock `readOnly` first (once per
|
|
154
|
+
* instance, so a re-lock after a remount cannot overwrite the snapshot with the stream's
|
|
155
|
+
* `readOnly: true`), then lock it.
|
|
156
|
+
*/
|
|
157
|
+
private takeOver;
|
|
158
|
+
/**
|
|
159
|
+
* Snapshot what the stream is about to overwrite in this model — once per model, just
|
|
160
|
+
* before the first stream write into it. Every model write path must claim first ({@link
|
|
161
|
+
* begin}'s clear, {@link attach}'s buffer replay, {@link append}'s edit): any of them can
|
|
162
|
+
* be the first to touch a model the stream has not seen, since the app may swap models or
|
|
163
|
+
* editors mid-stream.
|
|
164
|
+
*/
|
|
165
|
+
private claim;
|
|
166
|
+
/**
|
|
167
|
+
* The streamed buffer as stored in this model, preserving its displaced document's BOM.
|
|
168
|
+
*/
|
|
169
|
+
private valueFor;
|
|
170
|
+
/**
|
|
171
|
+
* Claim a model and bring it up to the complete buffered stream in one handle-stable write.
|
|
172
|
+
*/
|
|
173
|
+
private replay;
|
|
174
|
+
/**
|
|
175
|
+
* Hand every locked editor back with its own `readOnly` setting, via the handle recorded
|
|
176
|
+
* at lock time — a detached instance is restored too, not just the currently attached one.
|
|
177
|
+
* Drops both stream snapshots; the next stream starts fresh.
|
|
178
|
+
*/
|
|
179
|
+
private releaseAll;
|
|
70
180
|
/**
|
|
71
181
|
* Bind the mounted editor. If a stream is in progress, the buffered content is synced
|
|
72
182
|
* into the model immediately and the editor is set read-only.
|
|
73
183
|
*/
|
|
74
184
|
attach(instance: StreamTargetEditor): void;
|
|
75
185
|
/**
|
|
76
|
-
* Release the editor (unmount). A stream in progress keeps accumulating in the buffer
|
|
186
|
+
* Release the editor (unmount). A stream in progress keeps accumulating in the buffer, and
|
|
187
|
+
* the lock / displaced-document snapshots keep their handles — {@link end} and
|
|
188
|
+
* {@link discard} restore this instance and its model even while it stays detached.
|
|
77
189
|
*/
|
|
78
190
|
detach(instance: StreamTargetEditor): void;
|
|
79
191
|
/**
|
|
@@ -82,7 +194,8 @@ declare class StreamingEditorController {
|
|
|
82
194
|
*/
|
|
83
195
|
isStreaming(): boolean;
|
|
84
196
|
/**
|
|
85
|
-
* Start a stream: clears the buffer and the editor
|
|
197
|
+
* Start a stream: clears the buffer and the editor (snapshotting the document the clear
|
|
198
|
+
* displaces), and sets it read-only.
|
|
86
199
|
*/
|
|
87
200
|
begin(): void;
|
|
88
201
|
/**
|
|
@@ -92,22 +205,28 @@ declare class StreamingEditorController {
|
|
|
92
205
|
append(text: string): void;
|
|
93
206
|
/**
|
|
94
207
|
* End the stream and return the full streamed text — the caller reconciles it into app
|
|
95
|
-
* state (one write, replacing the per-token churn).
|
|
208
|
+
* state (one write, replacing the per-token churn). The streamed document stays on screen
|
|
209
|
+
* in the model that currently holds it; every OTHER model the stream wrote in passing (the
|
|
210
|
+
* app switched documents away and back mid-stream) gets its own displaced document back,
|
|
211
|
+
* instead of being stranded with an intermediate slice of the stream. Every editor the
|
|
212
|
+
* stream locked gets its own `readOnly` setting back, attached or not.
|
|
96
213
|
*/
|
|
97
214
|
end(): string;
|
|
98
215
|
/**
|
|
99
|
-
* Abort mid-stream WITHOUT yielding the buffer:
|
|
100
|
-
*
|
|
101
|
-
*
|
|
216
|
+
* Abort mid-stream WITHOUT yielding the buffer: drops the partial text, puts every
|
|
217
|
+
* displaced document back into the model it came from (even a detached one), and restores
|
|
218
|
+
* every locked editor's `readOnly` setting. For teardown paths (document switch / reset)
|
|
219
|
+
* where flushing the partial stream into the now-current document would corrupt it.
|
|
102
220
|
*/
|
|
103
221
|
discard(): void;
|
|
104
222
|
/**
|
|
105
223
|
* Exact find-and-replace on the attached editor, located by character offset and applied
|
|
106
224
|
* via `executeEdits` — preserving the undo stack, cursor, and fold state. Returns whether
|
|
107
|
-
* it was applied (`false` = no editor / empty search / no match
|
|
108
|
-
* a state
|
|
225
|
+
* it was applied (`false` = no editor / empty search / no match, or an editor that refused
|
|
226
|
+
* the edit because it is read-only — a stream in progress; the caller falls back to a state
|
|
227
|
+
* write).
|
|
109
228
|
*/
|
|
110
229
|
replace(search: string, replaceWith: string, all: boolean): boolean;
|
|
111
230
|
}
|
|
112
231
|
//#endregion
|
|
113
|
-
export { type MonacoHostOptions, type MonacoLanguage, type MonacoModule, type MonacoUiLocale, type StreamTargetEditor, StreamingEditorController, ensureMonacoHost };
|
|
232
|
+
export { type KeybindingTargetEditor, type MonacoBuiltInLanguage, type MonacoHostOptions, type MonacoLanguage, type MonacoModule, type MonacoUiLocale, type StreamTargetEditor, StreamingEditorController, type SwallowedKeybindings, disabledKeybindings, ensureMonacoHost, installDisabledKeybindings, monacoLanguages };
|
package/dist/index.js
CHANGED
|
@@ -43,17 +43,186 @@ function ensureMonacoHost(options = {}) {
|
|
|
43
43
|
return hostPromise;
|
|
44
44
|
}
|
|
45
45
|
//#endregion
|
|
46
|
+
//#region src/keybindings.ts
|
|
47
|
+
const FIND_CONTEXT_KEY = "inkstoneFindDisabled";
|
|
48
|
+
const COMMAND_PALETTE_CONTEXT_KEY = "inkstoneCommandPaletteDisabled";
|
|
49
|
+
const REGISTRY_KEY = Symbol.for("coldsmirk.inkstone.monaco.keybindingRegistry");
|
|
50
|
+
function landedChords(monaco) {
|
|
51
|
+
const holder = globalThis;
|
|
52
|
+
let registries = holder[REGISTRY_KEY];
|
|
53
|
+
if (!registries) {
|
|
54
|
+
registries = /* @__PURE__ */ new WeakMap();
|
|
55
|
+
holder[REGISTRY_KEY] = registries;
|
|
56
|
+
}
|
|
57
|
+
const existing = registries.get(monaco);
|
|
58
|
+
if (existing) return existing;
|
|
59
|
+
const created = /* @__PURE__ */ new Map();
|
|
60
|
+
registries.set(monaco, created);
|
|
61
|
+
return created;
|
|
62
|
+
}
|
|
63
|
+
function disabledKeybindings(monaco, disableFind, disableCommandPalette) {
|
|
64
|
+
const { KeyCode, KeyMod } = monaco;
|
|
65
|
+
return [...disableFind ? [{
|
|
66
|
+
contextKey: FIND_CONTEXT_KEY,
|
|
67
|
+
keybindings: [
|
|
68
|
+
KeyMod.CtrlCmd | KeyCode.KeyF,
|
|
69
|
+
KeyMod.CtrlCmd | KeyCode.KeyH,
|
|
70
|
+
KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyF
|
|
71
|
+
]
|
|
72
|
+
}] : [], ...disableCommandPalette ? [{
|
|
73
|
+
contextKey: COMMAND_PALETTE_CONTEXT_KEY,
|
|
74
|
+
keybindings: [KeyCode.F1]
|
|
75
|
+
}] : []];
|
|
76
|
+
}
|
|
77
|
+
function installDisabledKeybindings(monaco, editorInstance, groups) {
|
|
78
|
+
const registered = landedChords(monaco);
|
|
79
|
+
for (const { contextKey, keybindings } of groups) {
|
|
80
|
+
editorInstance.createContextKey(contextKey, true);
|
|
81
|
+
let landed = registered.get(contextKey);
|
|
82
|
+
if (!landed) {
|
|
83
|
+
landed = /* @__PURE__ */ new Set();
|
|
84
|
+
registered.set(contextKey, landed);
|
|
85
|
+
}
|
|
86
|
+
for (const keybinding of keybindings) if (!landed.has(keybinding) && editorInstance.addCommand(keybinding, () => void 0, contextKey) !== null) landed.add(keybinding);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const monacoLanguages = [
|
|
90
|
+
"abap",
|
|
91
|
+
"aes",
|
|
92
|
+
"apex",
|
|
93
|
+
"azcli",
|
|
94
|
+
"bat",
|
|
95
|
+
"bicep",
|
|
96
|
+
"c",
|
|
97
|
+
"cameligo",
|
|
98
|
+
"clojure",
|
|
99
|
+
"coffeescript",
|
|
100
|
+
"cpp",
|
|
101
|
+
"csharp",
|
|
102
|
+
"csp",
|
|
103
|
+
"css",
|
|
104
|
+
"cypher",
|
|
105
|
+
"dart",
|
|
106
|
+
"dockerfile",
|
|
107
|
+
"ecl",
|
|
108
|
+
"elixir",
|
|
109
|
+
"flow9",
|
|
110
|
+
"freemarker2",
|
|
111
|
+
"freemarker2.tag-angle.interpolation-bracket",
|
|
112
|
+
"freemarker2.tag-angle.interpolation-dollar",
|
|
113
|
+
"freemarker2.tag-auto.interpolation-bracket",
|
|
114
|
+
"freemarker2.tag-auto.interpolation-dollar",
|
|
115
|
+
"freemarker2.tag-bracket.interpolation-bracket",
|
|
116
|
+
"freemarker2.tag-bracket.interpolation-dollar",
|
|
117
|
+
"fsharp",
|
|
118
|
+
"go",
|
|
119
|
+
"graphql",
|
|
120
|
+
"handlebars",
|
|
121
|
+
"hcl",
|
|
122
|
+
"html",
|
|
123
|
+
"ini",
|
|
124
|
+
"java",
|
|
125
|
+
"javascript",
|
|
126
|
+
"json",
|
|
127
|
+
"julia",
|
|
128
|
+
"kotlin",
|
|
129
|
+
"less",
|
|
130
|
+
"lexon",
|
|
131
|
+
"liquid",
|
|
132
|
+
"lua",
|
|
133
|
+
"m3",
|
|
134
|
+
"markdown",
|
|
135
|
+
"mdx",
|
|
136
|
+
"mips",
|
|
137
|
+
"msdax",
|
|
138
|
+
"mysql",
|
|
139
|
+
"objective-c",
|
|
140
|
+
"pascal",
|
|
141
|
+
"pascaligo",
|
|
142
|
+
"perl",
|
|
143
|
+
"pgsql",
|
|
144
|
+
"php",
|
|
145
|
+
"pla",
|
|
146
|
+
"plaintext",
|
|
147
|
+
"postiats",
|
|
148
|
+
"powerquery",
|
|
149
|
+
"powershell",
|
|
150
|
+
"proto",
|
|
151
|
+
"pug",
|
|
152
|
+
"python",
|
|
153
|
+
"qsharp",
|
|
154
|
+
"r",
|
|
155
|
+
"razor",
|
|
156
|
+
"redis",
|
|
157
|
+
"redshift",
|
|
158
|
+
"restructuredtext",
|
|
159
|
+
"ruby",
|
|
160
|
+
"rust",
|
|
161
|
+
"sb",
|
|
162
|
+
"scala",
|
|
163
|
+
"scheme",
|
|
164
|
+
"scss",
|
|
165
|
+
"shell",
|
|
166
|
+
"sol",
|
|
167
|
+
"sparql",
|
|
168
|
+
"sql",
|
|
169
|
+
"st",
|
|
170
|
+
"swift",
|
|
171
|
+
"systemverilog",
|
|
172
|
+
"tcl",
|
|
173
|
+
"twig",
|
|
174
|
+
"typescript",
|
|
175
|
+
"typespec",
|
|
176
|
+
"vb",
|
|
177
|
+
"verilog",
|
|
178
|
+
"wgsl",
|
|
179
|
+
"xml",
|
|
180
|
+
"yaml"
|
|
181
|
+
].toSorted();
|
|
182
|
+
//#endregion
|
|
46
183
|
//#region src/streaming.ts
|
|
184
|
+
const UTF8_BOM = "";
|
|
47
185
|
var StreamingEditorController = class {
|
|
48
186
|
editor = null;
|
|
49
187
|
writing = false;
|
|
50
188
|
buffer = "";
|
|
189
|
+
locked = /* @__PURE__ */ new Map();
|
|
190
|
+
displaced = /* @__PURE__ */ new Map();
|
|
191
|
+
synced = null;
|
|
192
|
+
takeOver(instance) {
|
|
193
|
+
if (!this.locked.has(instance)) this.locked.set(instance, {
|
|
194
|
+
readOnly: instance.getRawOptions().readOnly ?? false,
|
|
195
|
+
watcher: instance.onDidDispose(() => this.locked.delete(instance))
|
|
196
|
+
});
|
|
197
|
+
instance.updateOptions({ readOnly: true });
|
|
198
|
+
}
|
|
199
|
+
claim(model) {
|
|
200
|
+
if (!this.displaced.has(model)) this.displaced.set(model, model.getValue(void 0, true));
|
|
201
|
+
}
|
|
202
|
+
valueFor(model) {
|
|
203
|
+
return this.displaced.get(model)?.startsWith(UTF8_BOM) ? UTF8_BOM + this.buffer : this.buffer;
|
|
204
|
+
}
|
|
205
|
+
replay(instance, model) {
|
|
206
|
+
this.claim(model);
|
|
207
|
+
this.synced = model;
|
|
208
|
+
model.setValue(this.valueFor(model));
|
|
209
|
+
if (!model.isDisposed() && instance.getModel() === model) instance.revealLine(model.getLineCount());
|
|
210
|
+
}
|
|
211
|
+
releaseAll() {
|
|
212
|
+
for (const [instance, lock] of this.locked) {
|
|
213
|
+
lock.watcher.dispose();
|
|
214
|
+
instance.updateOptions({ readOnly: lock.readOnly });
|
|
215
|
+
}
|
|
216
|
+
this.locked.clear();
|
|
217
|
+
this.displaced.clear();
|
|
218
|
+
this.synced = null;
|
|
219
|
+
}
|
|
51
220
|
attach(instance) {
|
|
52
221
|
this.editor = instance;
|
|
53
222
|
if (this.writing) {
|
|
54
|
-
|
|
55
|
-
instance.
|
|
56
|
-
|
|
223
|
+
this.takeOver(instance);
|
|
224
|
+
const model = instance.getModel();
|
|
225
|
+
if (model) this.replay(instance, model);
|
|
57
226
|
}
|
|
58
227
|
}
|
|
59
228
|
detach(instance) {
|
|
@@ -65,9 +234,11 @@ var StreamingEditorController = class {
|
|
|
65
234
|
begin() {
|
|
66
235
|
this.writing = true;
|
|
67
236
|
this.buffer = "";
|
|
237
|
+
this.synced = null;
|
|
68
238
|
if (this.editor) {
|
|
69
|
-
this.editor
|
|
70
|
-
this.editor.
|
|
239
|
+
this.takeOver(this.editor);
|
|
240
|
+
const model = this.editor.getModel();
|
|
241
|
+
if (model) this.replay(this.editor, model);
|
|
71
242
|
}
|
|
72
243
|
}
|
|
73
244
|
append(text) {
|
|
@@ -75,6 +246,11 @@ var StreamingEditorController = class {
|
|
|
75
246
|
this.buffer += text;
|
|
76
247
|
const model = this.editor?.getModel();
|
|
77
248
|
if (!this.editor || !model) return;
|
|
249
|
+
if (model !== this.synced) {
|
|
250
|
+
this.replay(this.editor, model);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.claim(model);
|
|
78
254
|
const line = model.getLineCount();
|
|
79
255
|
const column = model.getLineMaxColumn(line);
|
|
80
256
|
model.applyEdits([{
|
|
@@ -86,17 +262,28 @@ var StreamingEditorController = class {
|
|
|
86
262
|
},
|
|
87
263
|
text
|
|
88
264
|
}]);
|
|
89
|
-
this.editor.revealLine(model.getLineCount());
|
|
265
|
+
if (!model.isDisposed()) this.editor.revealLine(model.getLineCount());
|
|
90
266
|
}
|
|
91
267
|
end() {
|
|
92
|
-
|
|
93
|
-
|
|
268
|
+
try {
|
|
269
|
+
const instance = this.editor;
|
|
270
|
+
const current = instance?.getModel();
|
|
271
|
+
if (instance && current && current !== this.synced && !current.isDisposed()) this.replay(instance, current);
|
|
272
|
+
for (const [model, doc] of this.displaced) if (model !== this.synced && !model.isDisposed()) model.setValue(doc);
|
|
273
|
+
} finally {
|
|
274
|
+
this.writing = false;
|
|
275
|
+
this.releaseAll();
|
|
276
|
+
}
|
|
94
277
|
return this.buffer;
|
|
95
278
|
}
|
|
96
279
|
discard() {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
280
|
+
try {
|
|
281
|
+
for (const [model, doc] of this.displaced) if (!model.isDisposed()) model.setValue(doc);
|
|
282
|
+
} finally {
|
|
283
|
+
this.writing = false;
|
|
284
|
+
this.buffer = "";
|
|
285
|
+
this.releaseAll();
|
|
286
|
+
}
|
|
100
287
|
}
|
|
101
288
|
replace(search, replaceWith, all) {
|
|
102
289
|
const model = this.editor?.getModel();
|
|
@@ -125,9 +312,8 @@ var StreamingEditorController = class {
|
|
|
125
312
|
text: replaceWith
|
|
126
313
|
};
|
|
127
314
|
});
|
|
128
|
-
this.editor.executeEdits("inkstone-stream-replace", edits);
|
|
129
|
-
return true;
|
|
315
|
+
return this.editor.executeEdits("inkstone-stream-replace", edits);
|
|
130
316
|
}
|
|
131
317
|
};
|
|
132
318
|
//#endregion
|
|
133
|
-
export { StreamingEditorController, ensureMonacoHost };
|
|
319
|
+
export { StreamingEditorController, disabledKeybindings, ensureMonacoHost, installDisabledKeybindings, monacoLanguages };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coldsmirk/inkstone-monaco",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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, and a streaming editor controller for AI-typed content.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"monaco",
|
|
@@ -25,19 +25,11 @@
|
|
|
25
25
|
"type": "module",
|
|
26
26
|
"exports": {
|
|
27
27
|
".": {
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
"default": "./dist/index.js"
|
|
31
|
-
},
|
|
32
|
-
"require": {
|
|
33
|
-
"types": "./dist/index.d.cts",
|
|
34
|
-
"default": "./dist/index.cjs"
|
|
35
|
-
}
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
36
30
|
},
|
|
37
31
|
"./package.json": "./package.json"
|
|
38
32
|
},
|
|
39
|
-
"main": "./dist/index.cjs",
|
|
40
|
-
"module": "./dist/index.js",
|
|
41
33
|
"types": "./dist/index.d.ts",
|
|
42
34
|
"files": [
|
|
43
35
|
"dist"
|
|
@@ -49,7 +41,7 @@
|
|
|
49
41
|
"monaco-editor": "^0.55.1"
|
|
50
42
|
},
|
|
51
43
|
"engines": {
|
|
52
|
-
"node": ">=
|
|
44
|
+
"node": ">=24"
|
|
53
45
|
},
|
|
54
46
|
"publishConfig": {
|
|
55
47
|
"access": "public"
|
package/dist/index.cjs
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
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 };
|