@coldsmirk/inkstone-monaco 0.17.1 → 0.19.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
@@ -16,6 +16,7 @@ Part of [inkstone](https://github.com/coldsmirk/inkstone). For a drop-in React c
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.
17
17
  - **`replaceInEditor(editor, old, new, all)`** — exact find-and-replace on an editor handle via `executeEdits`, preserving undo/cursor/folds; returns whether the edit landed (`false` = no editor / empty search / no match / a read-only editor refused it — e.g. one locked by a stream in progress), so a tool-call handler knows to fall back to a state write.
18
18
  - **`registerShikiHighlighting(monaco, highlighter)`** — wire `@coldsmirk/inkstone-core`'s shared Shiki highlighter onto a monaco module, once per distinct module: registers the canonical grammar names as Monaco languages, applies `@shikijs/monaco`'s tokenizer/themes, and repairs its `setTheme` patch so non-Shiki theme ids (vs/vs-dark, caller-defined) keep Monaco's original path. A failed registration is restored and retried on the next call.
19
+ - **`installDesignTokens()`** — defines the VS Code design tokens monaco-editor 0.56's chrome reads and the standalone editor never does ([microsoft/monaco-editor#5408](https://github.com/microsoft/monaco-editor/issues/5408)): `--vscode-cornerRadius-{small,medium,large,xLarge}` and `--vscode-shadow-{md,lg,xl}`, on `.monaco-editor` (the context menu's shadow root and a host's `overflowWidgetsDomNode` inherit them). Without them the context menu, completion, hover, and find widgets ship square and shadowless. Each resolves through the host's `--inkstone-radius-*` / `--inkstone-shadow-*` token first (see `@coldsmirk/inkstone-core`), then a fallback scale in the spirit of 0.55's look. `ensureMonacoHost` installs it; idempotent.
19
20
  - **`DEFAULT_MONACO_OPTIONS`** — the inkstone construction-option baseline (no minimap, 14px/1.6 type on the shared `--inkstone-font-family-code` token, embedded-friendly find widget, tuned completion, thin scrollbars). Spread it to extend: `{ ...DEFAULT_MONACO_OPTIONS, wordWrap: "on" }`.
20
21
  - **`reconcileModelValue` / `modelValueMatches`** — the controlled-value reconcile: diff an external value against a live model (normalized to the model's EOL) and apply one minimal `applyEdits` replace that never lands on the undo stack or moves a caret outside the changed region.
21
22
 
package/dist/index.d.ts CHANGED
@@ -51,6 +51,56 @@ type MonacoModule = typeof monacoEditor;
51
51
  */
52
52
  declare function ensureMonacoHost(options?: MonacoHostOptions): Promise<MonacoModule>;
53
53
  //#endregion
54
+ //#region src/auto-imports.d.ts
55
+ /**
56
+ * Options for {@link installAutoImportCompletions}.
57
+ */
58
+ interface AutoImportCompletionOptions {
59
+ /**
60
+ * The module specifiers admitted as auto-import sources for this model, or undefined /
61
+ * empty to offer none. Consulted on every completion request, so the answer may follow the
62
+ * app's active editing surface.
63
+ */
64
+ admittedSources: (model: monacoEditor.editor.ITextModel) => readonly string[] | undefined;
65
+ }
66
+ /**
67
+ * Registers the auto-import completion provider on `monaco`'s `typescript` language id (the
68
+ * id also carries `.tsx` models — the model path decides JSX-ness). Additive beside the
69
+ * stock provider: entries are only unimported module exports from sources the app admits,
70
+ * labeled with their module, sorted after in-scope symbols by the language service's own
71
+ * `sortText`, and resolving one computes the import statement (merging into an existing
72
+ * import from the same module) as `additionalTextEdits`. A routed worker without the bundled
73
+ * subclass (a `getWorker` override) is detected on the first answer and the install disarms
74
+ * silently. Install once per app; repeated calls return the first install's disposable.
75
+ */
76
+ declare function installAutoImportCompletions(monaco: MonacoModule, options: AutoImportCompletionOptions): monacoEditor.IDisposable;
77
+ //#endregion
78
+ //#region src/design-tokens.d.ts
79
+ /**
80
+ * Define the VS Code design tokens Monaco's chrome reads but the standalone editor lacks —
81
+ * corner radii and widget shadows — on `.monaco-editor`, resolved through the host's
82
+ * `--inkstone-radius-*` / `--inkstone-shadow-*` tokens with inkstone's fallbacks. Installed by
83
+ * `ensureMonacoHost`; idempotent, so a host that brings monaco up on its own can call it too.
84
+ */
85
+ declare function installDesignTokens(): void;
86
+ //#endregion
87
+ //#region src/hover-dead-band.d.ts
88
+ /**
89
+ * The slice of a standalone editor the install touches — structural, so the install is
90
+ * testable without evaluating the monaco module (which jsdom cannot do).
91
+ */
92
+ interface HoverDeadBandTargetEditor {
93
+ getContribution: (id: string) => unknown;
94
+ }
95
+ /**
96
+ * Keeps `editorInstance`'s content hover reachable by a slowly moving pointer: overrides the
97
+ * controller's on-the-widget test so the widget's pointer-active frame counts as the widget
98
+ * (see the file comment for the dead band this compensates). Installed by `<MonacoEditor>`
99
+ * on every editor; a monaco whose hover controller no longer matches the pinned shape is
100
+ * left untouched. Dispose to restore the stock test.
101
+ */
102
+ declare function installHoverDeadBandFix(editorInstance: HoverDeadBandTargetEditor): monacoEditor.IDisposable;
103
+ //#endregion
54
104
  //#region src/jsx-tag-closing.d.ts
55
105
  /**
56
106
  * The slice of a text model the install reads — a structural type, so the install is testable
@@ -325,4 +375,4 @@ declare class StreamingEditorController {
325
375
  discard(): void;
326
376
  }
327
377
  //#endregion
328
- export { DEFAULT_MONACO_OPTIONS, type JsxTagClosingTargetEditor, type JsxTagClosingTargetModel, type KeybindingTargetEditor, type MonacoBuiltInLanguage, type MonacoHostOptions, type MonacoLanguage, type MonacoModule, type MonacoUiLocale, type StreamTargetEditor, StreamingEditorController, type SwallowedKeybindings, disabledKeybindings, ensureMonacoHost, installDisabledKeybindings, installJsxTagClosing, modelValueMatches, monacoLanguages, reconcileModelValue, registerShikiHighlighting, replaceInEditor };
378
+ export { type AutoImportCompletionOptions, DEFAULT_MONACO_OPTIONS, type HoverDeadBandTargetEditor, type JsxTagClosingTargetEditor, type JsxTagClosingTargetModel, type KeybindingTargetEditor, type MonacoBuiltInLanguage, type MonacoHostOptions, type MonacoLanguage, type MonacoModule, type MonacoUiLocale, type StreamTargetEditor, StreamingEditorController, type SwallowedKeybindings, disabledKeybindings, ensureMonacoHost, installAutoImportCompletions, installDesignTokens, installDisabledKeybindings, installHoverDeadBandFix, installJsxTagClosing, modelValueMatches, monacoLanguages, reconcileModelValue, registerShikiHighlighting, replaceInEditor };
package/dist/index.js CHANGED
@@ -1,5 +1,147 @@
1
- import { CODE_FONT_FAMILY } from "@coldsmirk/inkstone-core";
1
+ import { CODE_FONT_FAMILY, inkstoneRadius, inkstoneShadow } from "@coldsmirk/inkstone-core";
2
2
  import { shikiToMonaco } from "@shikijs/monaco";
3
+ //#region src/auto-imports.ts
4
+ const MISSING_LOOKUP_REJECTION$1 = "Missing requestHandler or method: getAutoImportCompletionsAtPosition";
5
+ const MODE_BOOTING_REJECTIONS$1 = /* @__PURE__ */ new Set(["TypeScript not registered!", "JavaScript not registered!"]);
6
+ const ENTRY_KINDS = {
7
+ function: "Function",
8
+ method: "Function",
9
+ class: "Class",
10
+ interface: "Interface",
11
+ module: "Module",
12
+ type: "Variable",
13
+ alias: "Variable"
14
+ };
15
+ function itemKind(monaco, kind) {
16
+ return monaco.languages.CompletionItemKind[ENTRY_KINDS[kind] ?? "Variable"];
17
+ }
18
+ function partsText(parts) {
19
+ return (parts ?? []).map((part) => part.text).join("");
20
+ }
21
+ const LEDGER_KEY = Symbol.for("coldsmirk.inkstone.autoImportCompletions");
22
+ function ledgerOf() {
23
+ const holder = globalThis;
24
+ let ledger = holder[LEDGER_KEY];
25
+ if (!ledger) {
26
+ ledger = /* @__PURE__ */ new WeakMap();
27
+ holder[LEDGER_KEY] = ledger;
28
+ }
29
+ return ledger;
30
+ }
31
+ function classify(error) {
32
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
33
+ if (message === MISSING_LOOKUP_REJECTION$1) return "disarm";
34
+ return MODE_BOOTING_REJECTIONS$1.has(message) ? "booting" : "failure";
35
+ }
36
+ function installAutoImportCompletions(monaco, options) {
37
+ const ledger = ledgerOf();
38
+ const installed = ledger.get(monaco);
39
+ if (installed) return installed;
40
+ let lookupUnsupported = false;
41
+ const disposable = monaco.languages.registerCompletionItemProvider("typescript", {
42
+ async provideCompletionItems(model, position) {
43
+ const sources = options.admittedSources(model);
44
+ if (lookupUnsupported || !sources || sources.length === 0) return { suggestions: [] };
45
+ const wordInfo = model.getWordUntilPosition(position);
46
+ const offset = model.getOffsetAt(position);
47
+ try {
48
+ const workerFor = await monaco.typescript.getTypeScriptWorker();
49
+ if (model.isDisposed()) return { suggestions: [] };
50
+ const info = await (await workerFor(model.uri)).getAutoImportCompletionsAtPosition(model.uri.toString(), offset, sources);
51
+ if (!info || model.isDisposed()) return { suggestions: [] };
52
+ const range = {
53
+ startLineNumber: position.lineNumber,
54
+ startColumn: wordInfo.startColumn,
55
+ endLineNumber: position.lineNumber,
56
+ endColumn: wordInfo.endColumn
57
+ };
58
+ return {
59
+ suggestions: info.entries.map((entry) => {
60
+ return {
61
+ label: {
62
+ label: entry.name,
63
+ description: entry.source
64
+ },
65
+ kind: itemKind(monaco, entry.kind),
66
+ insertText: entry.insertText ?? entry.name,
67
+ filterText: entry.name,
68
+ sortText: entry.sortText,
69
+ range,
70
+ uri: model.uri,
71
+ offset,
72
+ entry
73
+ };
74
+ }),
75
+ incomplete: info.isIncomplete
76
+ };
77
+ } catch (error) {
78
+ const verdict = classify(error);
79
+ if (verdict === "disarm") lookupUnsupported = true;
80
+ else if (verdict === "failure") console.error("inkstone: auto-import completion lookup failed", error);
81
+ return { suggestions: [] };
82
+ }
83
+ },
84
+ async resolveCompletionItem(item) {
85
+ const myItem = item;
86
+ if (!myItem.entry) return item;
87
+ try {
88
+ const details = await (await (await monaco.typescript.getTypeScriptWorker())(myItem.uri)).getAutoImportCompletionEntryDetails(myItem.uri.toString(), myItem.offset, myItem.entry.name, myItem.entry.source, myItem.entry.data);
89
+ const model = monaco.editor.getModel(myItem.uri);
90
+ if (!details || !model || model.isDisposed()) return item;
91
+ const fileName = myItem.uri.toString();
92
+ const additionalTextEdits = (details.codeActions ?? []).flatMap((action) => action.changes).filter((change) => change.fileName === fileName).flatMap((change) => change.textChanges).map((textChange) => {
93
+ const start = model.getPositionAt(textChange.span.start);
94
+ const end = model.getPositionAt(textChange.span.start + textChange.span.length);
95
+ return {
96
+ range: {
97
+ startLineNumber: start.lineNumber,
98
+ startColumn: start.column,
99
+ endLineNumber: end.lineNumber,
100
+ endColumn: end.column
101
+ },
102
+ text: textChange.newText
103
+ };
104
+ });
105
+ const documentation = partsText(details.documentation);
106
+ return {
107
+ ...myItem,
108
+ additionalTextEdits,
109
+ detail: details.codeActions?.[0]?.description ?? partsText(details.displayParts),
110
+ ...documentation && { documentation: { value: documentation } }
111
+ };
112
+ } catch (error) {
113
+ if (classify(error) === "failure") console.error("inkstone: auto-import details lookup failed", error);
114
+ return item;
115
+ }
116
+ }
117
+ });
118
+ const install = { dispose: () => {
119
+ ledger.delete(monaco);
120
+ disposable.dispose();
121
+ } };
122
+ ledger.set(monaco, install);
123
+ return install;
124
+ }
125
+ //#endregion
126
+ //#region src/design-tokens.ts
127
+ const DESIGN_TOKENS_STYLE_SELECTOR = "style[data-inkstone=\"monaco-design-tokens\"]";
128
+ const DESIGN_TOKENS = [
129
+ `--vscode-cornerRadius-small: ${inkstoneRadius("small", "2px")}`,
130
+ `--vscode-cornerRadius-medium: ${inkstoneRadius("medium", "4px")}`,
131
+ `--vscode-cornerRadius-large: ${inkstoneRadius("large", "6px")}`,
132
+ `--vscode-cornerRadius-xLarge: ${inkstoneRadius("xlarge", "8px")}`,
133
+ `--vscode-shadow-md: ${inkstoneShadow("medium", "0 1px 4px var(--vscode-widget-shadow)")}`,
134
+ `--vscode-shadow-lg: ${inkstoneShadow("large", "0 2px 8px var(--vscode-widget-shadow)")}`,
135
+ `--vscode-shadow-xl: ${inkstoneShadow("xlarge", "0 4px 16px var(--vscode-widget-shadow)")}`
136
+ ];
137
+ function installDesignTokens() {
138
+ if (document.querySelector(DESIGN_TOKENS_STYLE_SELECTOR)) return;
139
+ const style = document.createElement("style");
140
+ style.dataset.inkstone = "monaco-design-tokens";
141
+ style.textContent = `.monaco-editor { ${DESIGN_TOKENS.join("; ")}; }`;
142
+ document.head.append(style);
143
+ }
144
+ //#endregion
3
145
  //#region src/geometry-fixes.ts
4
146
  const HOVER_GEOMETRY_STYLE_SELECTOR = "style[data-inkstone=\"monaco-hover-geometry\"]";
5
147
  function installHoverGeometryFix() {
@@ -84,6 +226,7 @@ function ensureMonacoHost(options = {}) {
84
226
  let restoreNls;
85
227
  try {
86
228
  if (locale === "zh-cn") restoreNls = await applyZhCnCatalog();
229
+ installDesignTokens();
87
230
  installHoverGeometryFix();
88
231
  installSuggestGeometryFix();
89
232
  const monaco = await import("monaco-editor");
@@ -101,6 +244,29 @@ function ensureMonacoHost(options = {}) {
101
244
  return attempt;
102
245
  }
103
246
  //#endregion
247
+ //#region src/hover-dead-band.ts
248
+ const CONTENT_HOVER_CONTRIBUTION = "editor.contrib.contentHover";
249
+ const TOLERANCE = 4;
250
+ function isContentHoverController(candidate) {
251
+ return typeof candidate === "object" && candidate !== null && typeof candidate._isMouseOnContentHoverWidget === "function";
252
+ }
253
+ function installHoverDeadBandFix(editorInstance) {
254
+ const controller = editorInstance.getContribution(CONTENT_HOVER_CONTRIBUTION);
255
+ if (!isContentHoverController(controller) || Object.hasOwn(controller, "_isMouseOnContentHoverWidget")) return { dispose: () => {} };
256
+ controller._isMouseOnContentHoverWidget = (mouseEvent) => {
257
+ const dom = controller._contentWidget?.getDomNode();
258
+ if (!dom) return false;
259
+ const rect = dom.getBoundingClientRect();
260
+ const view = dom.ownerDocument.defaultView;
261
+ const x = mouseEvent.event.posx - (view?.scrollX ?? 0);
262
+ const y = mouseEvent.event.posy - (view?.scrollY ?? 0);
263
+ return rect.width > 0 && rect.height > 0 && x >= rect.left - TOLERANCE && x <= rect.right + TOLERANCE && y >= rect.top - TOLERANCE && y <= rect.bottom + TOLERANCE;
264
+ };
265
+ return { dispose: () => {
266
+ delete controller._isMouseOnContentHoverWidget;
267
+ } };
268
+ }
269
+ //#endregion
104
270
  //#region src/jsx-tag-closing.ts
105
271
  const MISSING_LOOKUP_REJECTION = "Missing requestHandler or method: getJsxClosingTagAtPosition";
106
272
  const MODE_BOOTING_REJECTIONS = /* @__PURE__ */ new Set(["TypeScript not registered!", "JavaScript not registered!"]);
@@ -520,4 +686,4 @@ var StreamingEditorController = class {
520
686
  }
521
687
  };
522
688
  //#endregion
523
- export { DEFAULT_MONACO_OPTIONS, StreamingEditorController, disabledKeybindings, ensureMonacoHost, installDisabledKeybindings, installJsxTagClosing, modelValueMatches, monacoLanguages, reconcileModelValue, registerShikiHighlighting, replaceInEditor };
689
+ export { DEFAULT_MONACO_OPTIONS, StreamingEditorController, disabledKeybindings, ensureMonacoHost, installAutoImportCompletions, installDesignTokens, installDisabledKeybindings, installHoverDeadBandFix, installJsxTagClosing, modelValueMatches, monacoLanguages, reconcileModelValue, registerShikiHighlighting, replaceInEditor };
@@ -2,16 +2,65 @@
2
2
  // See editor.worker.js for why this indirection exists.
3
3
  //
4
4
  // Unlike its siblings this entry is not a bare re-export: it subclasses the stock worker to
5
- // expose `getJsxClosingTagAtPosition` — the language-service call behind auto-closing JSX
6
- // tags (`installJsxTagClosing` is the editor-side half) which monaco's own worker never
7
- // proxied. The host's worker proxy forwards any method name, so the subclass only has to
8
- // exist here; nothing on the main thread changes.
5
+ // expose language-service calls monaco's own worker never proxied — the host's worker proxy
6
+ // forwards any method name, so the subclass only has to exist here; nothing on the main
7
+ // thread changes for it to be callable:
8
+ // - `getJsxClosingTagAtPosition`, behind auto-closing JSX tags (`installJsxTagClosing` is
9
+ // the editor-side half);
10
+ // - the auto-import completion pair, behind `installAutoImportCompletions`: the stock worker
11
+ // calls `getCompletionsAtPosition` without preferences, so the service never surfaces
12
+ // unimported module exports, and its details call drops `source`/`data`, so the import
13
+ // text edit (`codeActions`) can never be requested. These two pass the module-export
14
+ // preferences through and return the entries verbatim.
9
15
  import { initialize, TypeScriptWorker } from "monaco-editor/language/typescript/ts.worker.js";
10
16
 
17
+ // The preferences that make the language service surface unimported module exports and, on
18
+ // the details call, compute their import edits. `allowIncompleteCompletions` lets it bound a
19
+ // broad enumeration by the typed prefix and say so (`isIncomplete` → monaco re-queries).
20
+ const AUTO_IMPORT_PREFERENCES = {
21
+ includeCompletionsForModuleExports: true,
22
+ includeCompletionsWithInsertText: true,
23
+ allowIncompleteCompletions: true
24
+ };
25
+
11
26
  class JsxTypeScriptWorker extends TypeScriptWorker {
12
27
  getJsxClosingTagAtPosition(fileName, position) {
13
28
  return this.getLanguageService().getJsxClosingTagAtPosition(fileName, position);
14
29
  }
30
+
31
+ getAutoImportCompletionsAtPosition(fileName, position, admittedSources) {
32
+ const info = this.getLanguageService().getCompletionsAtPosition(fileName, position, AUTO_IMPORT_PREFERENCES);
33
+
34
+ if (!info) {
35
+ return;
36
+ }
37
+
38
+ // Only entries importable from an admitted specifier: the language service also offers
39
+ // every other module in the program (deep package paths included), which the editor-side
40
+ // policy has ruled out — filtering here keeps them off the wire entirely.
41
+ const admitted = new Set(admittedSources);
42
+
43
+ return {
44
+ isIncomplete: info.isIncomplete === true,
45
+ entries: info.entries.filter(entry => entry.source !== undefined && admitted.has(entry.source))
46
+ };
47
+ }
48
+
49
+ getAutoImportCompletionEntryDetails(fileName, position, entryName, source, data) {
50
+ // `data` must round-trip from the completion entry: without it the service cannot compute
51
+ // the import edit for a node_modules package (an ambient module happens to survive, but
52
+ // the contract is the pair). `semicolons: "insert"` makes the generated statement end in
53
+ // one, matching the house style of every template the editors seed.
54
+ return this.getLanguageService().getCompletionEntryDetails(
55
+ fileName,
56
+ position,
57
+ entryName,
58
+ { semicolons: "insert" },
59
+ source,
60
+ AUTO_IMPORT_PREFERENCES,
61
+ data
62
+ );
63
+ }
15
64
  }
16
65
 
17
66
  // Monaco's worker handshake is two-staged (`internal/common/initialize.js`, 0.56): a first
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldsmirk/inkstone-monaco",
3
- "version": "0.17.1",
3
+ "version": "0.19.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, 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.1"
43
+ "@coldsmirk/inkstone-core": "^0.19.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "monaco-editor": "^0.56.0",