@coldsmirk/inkstone-monaco 0.8.2 → 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 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 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
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 = "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>);
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, and sets it read-only.
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: 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.
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; the caller falls back to
108
- * a state write).
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
- instance.updateOptions({ readOnly: true });
55
- instance.setValue(this.buffer);
56
- instance.revealLine(instance.getModel()?.getLineCount() ?? 1);
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.updateOptions({ readOnly: true });
70
- this.editor.setValue("");
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
- this.writing = false;
93
- this.editor?.updateOptions({ readOnly: false });
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
- this.writing = false;
98
- this.buffer = "";
99
- this.editor?.updateOptions({ readOnly: false });
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.8.2",
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
- "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
- }
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": ">=22"
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 };