@coldsmirk/inkstone-monaco 0.17.0 → 0.17.1

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
@@ -10,7 +10,7 @@ Part of [inkstone](https://github.com/coldsmirk/inkstone). For a drop-in React c
10
10
  - **`getWorker` extension point** — plug third-party language workers (e.g. monaco-yaml) into the routing; return `undefined` to fall back to the built-in routes.
11
11
  - **`monacoLanguages`** — the sorted catalog of every built-in language id the installed Monaco registers (with the `MonacoBuiltInLanguage` union type behind it), held to the installed `monaco-editor` by an upgrade-tripwire test. These are *ids*, not display names (`proto`, `sol`, `aes`); an unknown id silently falls back to `plaintext`.
12
12
  - **`disabledKeybindings` / `installDisabledKeybindings`** — swallow the find/replace chords and the command palette's F1 for editors that opt in. Monaco's standalone keybinding service is page-global and its rules are permanent, so the install registers each chord once per monaco module's service (duplicate copies of this package share the ledger; independent monaco bundles register their own) and scopes it with a per-capability context key created on the opted-in editor; the chord table is held to the installed `monaco-editor` by an upgrade-tripwire test.
13
- - **`installJsxTagClosing`** — VS Code's auto-closing JSX tags: a typed `>` that completes an opening tag inserts the matching closing tag and keeps the caret between the pair. Parser-backed: the bundled TS worker subclasses monaco's stock worker to expose `getJsxClosingTagAtPosition` — a language-service call monaco never proxied (VS Code ships the feature in its TypeScript extension, outside the editor core) — so generics, comparisons, arrow functions, and already-closed elements never trigger. The keystroke handler gates itself to TypeScript/JavaScript models whose path ends in `.tsx` / `.jsx`; a `getWorker` override that routes the `typescript` label to a worker without the subclass downgrades the feature to a no-op. `<MonacoEditor>` installs it automatically.
13
+ - **`installJsxTagClosing`** — VS Code's auto-closing JSX tags: a typed `>` that completes an opening tag inserts the matching closing tag and keeps the caret between the pair. Parser-backed: the bundled TS worker subclasses monaco's stock worker to expose `getJsxClosingTagAtPosition` — a language-service call monaco never proxied (VS Code ships the feature in its TypeScript extension, outside the editor core) — so generics, comparisons, arrow functions, and already-closed elements never trigger. Only a *typed* `>` triggers: the gates require one bare single-character insertion into a **writable** TypeScript/JavaScript model whose path ends in `.tsx` / `.jsx`, and the reply lands only while the caret still sits right after that `>` — so `replaceInEditor` replacements, `StreamingEditorController` appends (the stream holds the editor read-only), and other programmatic writes never earn a closing tag, and the insert arrives as its own undo step (one undo removes just the closing tag). A `getWorker` override that routes the `typescript` label to a worker without the subclass is detected on its first lookup and the install disarms silently. Multi-cursor typing and the diff editor's panes are deliberately out of scope. `<MonacoEditor>` installs it automatically.
14
14
  - **`dropMonacoWorkerFallbacks()`** (from **`@coldsmirk/inkstone-monaco/vite`**) — a Vite plugin that removes monaco's built-in worker fallbacks at build time. Monaco's language workerManagers statically reference their workers as `new Worker(new URL(...))` fallbacks that never run under `ensureMonacoHost` (the environment check prefers `getWorker`), yet bundlers emit a chunk per reference — including a ~7MB TypeScript worker duplicate that is never fetched. The plugin rewrites the fallbacks into located throws so the duplicates are never emitted; if a monaco bump reshapes the pattern it warns and leaves the file untouched (the dead chunk returns, nothing breaks).
15
15
  - **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`.
16
16
  - **`StreamingEditorController`** — stream generated text into an editor without routing every token through app state: `begin()` / `append(chunk)` / `end()` / `discard()`, `attach(editor)` / `detach(editor)` with buffered replay for late-mounting editors (and for a model swapped onto the attached editor mid-stream, including after the final chunk), and `isStreaming()` for the onChange guard. The stream locks every editor it touches read-only and `end()` / `discard()` restore each one's own setting — by handle, so an editor detached mid-stream is restored too; `discard()` puts every displaced document back into the model it came from, and `end()` does the same for every model except the one keeping the streamed text. Full-buffer writes and restorations preserve each model's UTF-8 BOM. Editors and models the app disposed mid-stream are skipped, never touched — teardown can race the stream without wedging it.
package/dist/index.d.ts CHANGED
@@ -63,22 +63,37 @@ interface JsxTagClosingTargetModel {
63
63
  isDisposed: () => boolean;
64
64
  getPositionAt: (offset: number) => monacoEditor.IPosition;
65
65
  }
66
+ /**
67
+ * The caret shape the reply-time guard reads — the four corners of a (possibly collapsed)
68
+ * selection, as monaco's `Selection` carries them.
69
+ */
70
+ interface JsxTagClosingCursor {
71
+ readonly selectionStartLineNumber: number;
72
+ readonly selectionStartColumn: number;
73
+ readonly positionLineNumber: number;
74
+ readonly positionColumn: number;
75
+ }
66
76
  /**
67
77
  * The slice of a standalone editor the install touches — structural, same rationale as
68
78
  * {@link JsxTagClosingTargetModel}.
69
79
  */
70
80
  interface JsxTagClosingTargetEditor {
71
81
  getModel: () => JsxTagClosingTargetModel | null;
82
+ getOption: (id: monacoEditor.editor.EditorOption) => unknown;
83
+ getSelection: () => JsxTagClosingCursor | null;
84
+ pushUndoStop: () => boolean;
72
85
  onDidChangeModelContent: (listener: (event: monacoEditor.editor.IModelContentChangedEvent) => void) => monacoEditor.IDisposable;
73
86
  executeEdits: (source: string, edits: monacoEditor.editor.IIdentifiedSingleEditOperation[], endCursorState: monacoEditor.Selection[]) => boolean;
74
87
  }
75
88
  /**
76
89
  * Watch `editorInstance` for tag-completing `">"` keystrokes and insert the matching JSX
77
- * closing tag, keeping the cursor between the pair. Installed by `<MonacoEditor>` on every
78
- * editor; the keystroke handler gates itself, so only TypeScript / JavaScript models whose
79
- * path ends in `.tsx` / `.jsx` — the extensions that make the language service parse JSX —
80
- * ever reach the worker. Listener lifetime follows the editor; dispose the returned
81
- * subscription to detach earlier.
90
+ * closing tag as its own undo step, keeping the cursor between the pair. Installed by
91
+ * `<MonacoEditor>` on every editor; the keystroke handler gates itself, so only a bare ">"
92
+ * typed into a writable TypeScript / JavaScript model whose path ends in `.tsx` / `.jsx` —
93
+ * the extensions that make the language service parse JSX ever reaches the worker. A
94
+ * routed worker without the lookup (a `getWorker` override bypassing the bundled entry) is
95
+ * detected on its first answer and the install disarms silently. Listener lifetime follows
96
+ * the editor; dispose the returned subscription to detach earlier.
82
97
  */
83
98
  declare function installJsxTagClosing(monaco: MonacoModule, editorInstance: JsxTagClosingTargetEditor): monacoEditor.IDisposable;
84
99
  //#endregion
package/dist/index.js CHANGED
@@ -102,12 +102,16 @@ function ensureMonacoHost(options = {}) {
102
102
  }
103
103
  //#endregion
104
104
  //#region src/jsx-tag-closing.ts
105
+ const MISSING_LOOKUP_REJECTION = "Missing requestHandler or method: getJsxClosingTagAtPosition";
106
+ const MODE_BOOTING_REJECTIONS = /* @__PURE__ */ new Set(["TypeScript not registered!", "JavaScript not registered!"]);
105
107
  function installJsxTagClosing(monaco, editorInstance) {
108
+ let lookupUnsupported = false;
106
109
  return editorInstance.onDidChangeModelContent((event) => {
107
- if (event.isFlush || event.isUndoing || event.isRedoing || event.changes.length !== 1) return;
110
+ if (lookupUnsupported || event.isFlush || event.isUndoing || event.isRedoing || event.changes.length !== 1) return;
108
111
  const change = event.changes[0];
109
112
  const model = editorInstance.getModel();
110
- if (change?.text !== ">" || !model) return;
113
+ if (change?.text !== ">" || change.rangeLength !== 0 || !model) return;
114
+ if (editorInstance.getOption(monaco.editor.EditorOption.readOnly) === true) return;
111
115
  const { path } = model.uri;
112
116
  if (!path.endsWith(".tsx") && !path.endsWith(".jsx")) return;
113
117
  const language = model.getLanguageId();
@@ -116,16 +120,26 @@ function installJsxTagClosing(monaco, editorInstance) {
116
120
  const version = model.getVersionId();
117
121
  (async () => {
118
122
  try {
119
- const closing = await (await (language === "typescript" ? await monaco.typescript.getTypeScriptWorker() : await monaco.typescript.getJavaScriptWorker())(model.uri)).getJsxClosingTagAtPosition?.(model.uri.toString(), offset);
123
+ const closing = await (await (language === "typescript" ? await monaco.typescript.getTypeScriptWorker() : await monaco.typescript.getJavaScriptWorker())(model.uri)).getJsxClosingTagAtPosition(model.uri.toString(), offset);
120
124
  if (!closing || model.isDisposed() || model.getVersionId() !== version || editorInstance.getModel() !== model) return;
121
125
  const position = model.getPositionAt(offset);
126
+ const selection = editorInstance.getSelection();
127
+ if (!selection || selection.selectionStartLineNumber !== position.lineNumber || selection.selectionStartColumn !== position.column || selection.positionLineNumber !== position.lineNumber || selection.positionColumn !== position.column) return;
122
128
  const cursor = new monaco.Selection(position.lineNumber, position.column, position.lineNumber, position.column);
129
+ editorInstance.pushUndoStop();
123
130
  editorInstance.executeEdits("jsx-tag-closing", [{
124
131
  range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
125
132
  text: closing.newText,
126
133
  forceMoveMarkers: false
127
134
  }], [cursor]);
135
+ editorInstance.pushUndoStop();
128
136
  } catch (error) {
137
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
138
+ if (message === MISSING_LOOKUP_REJECTION) {
139
+ lookupUnsupported = true;
140
+ return;
141
+ }
142
+ if (MODE_BOOTING_REJECTIONS.has(message)) return;
129
143
  console.error("inkstone: jsx closing-tag lookup failed", error);
130
144
  }
131
145
  })();
package/dist/vite.js CHANGED
@@ -4,7 +4,7 @@ function dropMonacoWorkerFallbacks() {
4
4
  return {
5
5
  name: "inkstone:drop-monaco-worker-fallbacks",
6
6
  transform(code, id) {
7
- if (!id.includes("monaco-editor") || !id.endsWith("workerManager.js")) return;
7
+ if (!id.includes("/node_modules/monaco-editor/") || !id.endsWith("workerManager.js")) return;
8
8
  let matched = false;
9
9
  const transformed = code.replaceAll(WORKER_FALLBACK, (_expression, file) => {
10
10
  matched = true;
@@ -16,7 +16,7 @@ function dropMonacoWorkerFallbacks() {
16
16
  }
17
17
  return {
18
18
  code: transformed,
19
- map: null
19
+ map: { mappings: "" }
20
20
  };
21
21
  }
22
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldsmirk/inkstone-monaco",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Framework-agnostic Monaco assembly: lazy offline host (all language workers bundled, no CDN), opt-in Simplified-Chinese UI via monaco's official NLS catalog, a streaming editor controller for AI-typed content, and the shared Shiki highlighting bridge.",
5
5
  "keywords": [
6
6
  "monaco",
@@ -40,7 +40,7 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "@shikijs/monaco": "^4.4.3",
43
- "@coldsmirk/inkstone-core": "^0.17.0"
43
+ "@coldsmirk/inkstone-core": "^0.17.1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "monaco-editor": "^0.56.0",