@coldsmirk/inkstone-react 0.30.0 → 0.32.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
@@ -1,6 +1,6 @@
1
1
  # @coldsmirk/inkstone-react
2
2
 
3
- Drop-in React code editors with the integration chores solved once: a preconfigured Monaco **`<MonacoEditor>`** (offline workers, Shiki catppuccin themes, dark switching, opt-in zh-CN UI) and a controlled CodeMirror 6 **`<CodeMirrorEditor>`** (catppuccin themes, restyled chrome, MiniJinja support, sizing) — plus **`<MonacoDiffEditor>`** and **`<CodeMirrorDiffEditor>`**, the same assemblies around each engine's side-by-side diff view. Drop in a component; the plumbing is already wired.
3
+ Drop-in React code editors with the integration chores solved once: a preconfigured Monaco **`<MonacoEditor>`** (offline workers, Shiki catppuccin themes, dark switching, opt-in zh-CN UI) and a controlled CodeMirror 6 **`<CodeMirrorEditor>`** (catppuccin themes, restyled chrome, MiniJinja support, sizing) — plus **`<MonacoDiffEditor>`** and **`<CodeMirrorDiffEditor>`**, the same assemblies around each engine's side-by-side diff view, and **`<MonacoConsoleEditor>`**, the run-key surface inkstone's database editors stand on. Drop in a component; the plumbing is already wired.
4
4
 
5
5
  Part of [inkstone](https://github.com/coldsmirk/inkstone) — this package composes [`@coldsmirk/inkstone-core`](https://www.npmjs.com/package/@coldsmirk/inkstone-core) (shared Shiki highlighter), [`@coldsmirk/inkstone-monaco`](https://www.npmjs.com/package/@coldsmirk/inkstone-monaco) (offline Monaco host), and [`@coldsmirk/inkstone-codemirror`](https://www.npmjs.com/package/@coldsmirk/inkstone-codemirror) (controlled CM6 host + MiniJinja). See the [repository README](https://github.com/coldsmirk/inkstone#readme) for the full guide; the highlights below are what you touch daily.
6
6
 
@@ -31,7 +31,7 @@ import { MonacoEditor } from "@coldsmirk/inkstone-react/monaco";
31
31
  One component brings: the local `monaco-editor` instance (no CDN), bundled offline workers, Shiki catppuccin highlighting registered before first paint, light/dark switching via `dark`, and VS Code's auto-closing JSX tags on TypeScript/JavaScript models whose `path` ends in `.tsx` / `.jsx` (parser-backed via the bundled TS worker).
32
32
 
33
33
  - **Locale** — chrome speaks stock English; `locale="zh-cn"` switches to Monaco's official Simplified-Chinese catalog. Page-global and locked by the first editor to mount, so give every editor the same value.
34
- - **Type & metrics** — `fontSize` defaults to 14, `lineHeight` to 1.6 (Monaco's convention: values below 8 multiply the font size); `DEFAULT_MONACO_OPTIONS` is exported for hosts that need the same baseline.
34
+ - **Type & metrics** — `fontSize` defaults to 14, `lineHeight` to 1.6 (Monaco's convention: values below 8 multiply the font size); `DEFAULT_MONACO_OPTIONS` is exported for hosts that need the same baseline. `lineWrapping` (opt-in) soft-wraps long lines and ends each wrapped line in Monaco's ↩ (`wordWrapIndicator`), so a soft wrap reads apart from a real line break — on `<MonacoDiffEditor>` too.
35
35
  - **Loading / failure** — the host, the highlighter, and the Monaco chunk each have a loading window; `loading` and `failure` render nodes for them. A host load failure is not cached — a fresh mount retries.
36
36
  - **Embedded-friendly** — `find.addExtraSpaceOnTop` is off by default and the find-widget tooltip geometry of [monaco-editor#5177](https://github.com/microsoft/monaco-editor/issues/5177) is worked around. `disableContextMenu` / `disableFind` / `disableCommandPalette` switch off Monaco's browsing chrome per editor.
37
37
  - **Escape hatches** — `options` (raw `IStandaloneEditorConstructionOptions`), `hostOptions` (e.g. a custom `getWorker`; first mount wins), `beforeMount` / `onMount` for language-service configuration.
@@ -83,6 +83,30 @@ Each component keeps its engine's conventions from the single-editor sibling:
83
83
  - **`<MonacoDiffEditor>`** — the `<MonacoEditor>` assembly (offline host, Shiki themes, `locale`, `loading` / `failure`, `hostOptions`) around `monaco.editor.createDiffEditor`. `options` takes raw `IDiffEditorConstructionOptions` over the same `DEFAULT_MONACO_OPTIONS` baseline — e.g. `renderSideBySide: false` for the inline view.
84
84
  - **`<CodeMirrorDiffEditor>`** — `@codemirror/merge`'s `MergeView` behind `<CodeMirrorEditor>`'s create-once discipline, with the catppuccin flavors extended to the diff chrome (red deletions / green insertions, themed collapse bars and revert controls). `revertControls` adds per-chunk arrows copying the original chunk into the modified document (flows through `onChange`); `language`, `extensions`, and `dynamicExtensions` apply to both sides; `onMount` hands you the `MergeView`.
85
85
 
86
+ ## `<MonacoConsoleEditor>`
87
+
88
+ ```tsx
89
+ import { MonacoConsoleEditor } from "@coldsmirk/inkstone-react/monaco";
90
+
91
+ <MonacoConsoleEditor
92
+ ariaLabel="Query"
93
+ language="kql"
94
+ namespace="app-kql"
95
+ extension="kql"
96
+ value={query}
97
+ onChange={setQuery}
98
+ onRun={caret => run(caret)}
99
+ context={{ assist }}
100
+ register={({ monaco, path, context }) => registerKql(monaco, path, { assist: () => context().assist })}
101
+ />;
102
+ ```
103
+
104
+ The surface of a console — a document an operator writes and runs: `<MonacoEditor>` with line numbers and a model path of its own, the run key (Ctrl/Cmd-Enter) handing `onRun` the live caret (`{ text, from, to }`, the text and the offsets of one instant), `onCaret` as the caret moves, an `onReady` handle that inserts at the caret and focuses, and an optional mark on the span a run would send (`highlight`). The language editors of `@coldsmirk/inkstone-sql`, `-redis`, `-mongodb` and `-elasticsearch` are this component and their language's wiring.
105
+
106
+ - **Your language's wiring** — `beforeMount` defines a grammar before the editor exists; `register` makes the language's registrations for this editor once, as it mounts, and returns their disposables, which go with the editor. Whatever a registration reads that can change between renders goes in `context` and is read at the moment it is needed through the registration's `context()` getter — never captured at mount, so a completion after a connection switch answers from the new connection.
107
+ - **Names** — `namespace` names what the editor puts into Monaco: the model path `file:///<namespace>/<n>.<extension>`, the run action `<namespace>.run`, the inserts' source `<namespace>-insert`, and the mark's default class `<namespace>-runnable`.
108
+ - **`options`** — merged under the editor's own (`ariaLabel`) on its identity: keep the object stable, since a new one is applied to the live editor again.
109
+
86
110
  ## License
87
111
 
88
112
  UNLICENSED — proprietary. All rights reserved; no use, copying, or redistribution without the author's permission.
@@ -1,2 +1 @@
1
- import { a as CodeMirrorDiffEditor, i as CodeMirrorEditor, n as normalizeContext, r as searchPhrasesZhCn, t as codeMirrorLanguages } from "./codemirror-bROXidfr.js";
2
- export { CodeMirrorDiffEditor, CodeMirrorEditor, codeMirrorLanguages, normalizeContext, searchPhrasesZhCn };
1
+ export { a as CodeMirrorDiffEditor, i as CodeMirrorEditor, t as codeMirrorLanguages, n as normalizeContext, r as searchPhrasesZhCn } from "./codemirror-bROXidfr.js";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { a as codeMirrorLanguages, c as CodeMirrorEditor, d as CodeMirrorDiffEditorProps, i as SqlSchema, l as CodeMirrorEditorProps, n as EditorContext, o as normalizeContext, r as MergeView, s as searchPhrasesZhCn, t as CodeMirrorLanguage, u as CodeMirrorDiffEditor } from "./codemirror-BW_zSOVE.js";
2
- import { a as MonacoEditorProps, i as MonacoEditor, n as MonacoLanguage, o as MonacoDiffEditor, r as monacoLanguages, s as MonacoDiffEditorProps, t as DEFAULT_MONACO_OPTIONS } from "./monaco-OhCg_705.js";
2
+ import { a as MonacoEditorProps, c as ConsoleCaret, d as ConsoleRegistration, f as MonacoConsoleEditor, i as MonacoEditor, l as ConsoleEditorHandle, n as MonacoLanguage, o as MonacoDiffEditor, p as MonacoConsoleEditorProps, r as monacoLanguages, s as MonacoDiffEditorProps, t as DEFAULT_MONACO_OPTIONS, u as ConsoleEditorProps } from "./monaco-DTLlEEfk.js";
3
3
  import { n as useShikiHighlighter, t as ShikiState } from "./shiki-Ds-uZ7XH.js";
4
- export { CodeMirrorDiffEditor, type CodeMirrorDiffEditorProps, CodeMirrorEditor, type CodeMirrorEditorProps, type CodeMirrorLanguage, DEFAULT_MONACO_OPTIONS, type EditorContext, type MergeView, MonacoDiffEditor, type MonacoDiffEditorProps, MonacoEditor, type MonacoEditorProps, type MonacoLanguage, type ShikiState, type SqlSchema, codeMirrorLanguages, monacoLanguages, normalizeContext, searchPhrasesZhCn, useShikiHighlighter };
4
+ export { CodeMirrorDiffEditor, type CodeMirrorDiffEditorProps, CodeMirrorEditor, type CodeMirrorEditorProps, type CodeMirrorLanguage, type ConsoleCaret, type ConsoleEditorHandle, type ConsoleEditorProps, type ConsoleRegistration, DEFAULT_MONACO_OPTIONS, type EditorContext, type MergeView, MonacoConsoleEditor, type MonacoConsoleEditorProps, MonacoDiffEditor, type MonacoDiffEditorProps, MonacoEditor, type MonacoEditorProps, type MonacoLanguage, type ShikiState, type SqlSchema, codeMirrorLanguages, monacoLanguages, normalizeContext, searchPhrasesZhCn, useShikiHighlighter };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as CodeMirrorDiffEditor, i as CodeMirrorEditor, n as normalizeContext, r as searchPhrasesZhCn, t as codeMirrorLanguages } from "./codemirror-bROXidfr.js";
2
- import { i as MonacoDiffEditor, n as monacoLanguages, r as MonacoEditor, t as DEFAULT_MONACO_OPTIONS } from "./monaco-FFb6p4ut.js";
2
+ import { a as MonacoEditor, i as MonacoConsoleEditor, n as monacoLanguages, r as MonacoDiffEditor, t as DEFAULT_MONACO_OPTIONS } from "./monaco-CHi4aRfZ.js";
3
3
  import { t as useShikiHighlighter } from "./use-shiki-highlighter-CzSQcX2f.js";
4
4
  import "./shiki.js";
5
- export { CodeMirrorDiffEditor, CodeMirrorEditor, DEFAULT_MONACO_OPTIONS, MonacoDiffEditor, MonacoEditor, codeMirrorLanguages, monacoLanguages, normalizeContext, searchPhrasesZhCn, useShikiHighlighter };
5
+ export { CodeMirrorDiffEditor, CodeMirrorEditor, DEFAULT_MONACO_OPTIONS, MonacoConsoleEditor, MonacoDiffEditor, MonacoEditor, codeMirrorLanguages, monacoLanguages, normalizeContext, searchPhrasesZhCn, useShikiHighlighter };
@@ -59,123 +59,12 @@ function useMonacoHost(locale, hostOptions) {
59
59
  return state;
60
60
  }
61
61
  //#endregion
62
- //#region src/monaco-diff-editor.tsx
63
- function diffDocumentOptions({ lineWrapping, showLineNumbers, readOnly, collapseUnchanged, fontSize, lineHeight }) {
64
- const resolved = {
65
- wordWrap: lineWrapping ? "on" : "off",
66
- lineNumbers: showLineNumbers ? "on" : "off",
67
- readOnly,
68
- hideUnchangedRegions: { enabled: collapseUnchanged }
69
- };
70
- if (fontSize !== void 0) resolved.fontSize = fontSize;
71
- if (lineHeight !== void 0) resolved.lineHeight = lineHeight;
72
- return resolved;
73
- }
74
- function reconcilePane(model, value, reconciling) {
75
- if (!model) return;
76
- reconciling.current = true;
77
- try {
78
- reconcileModelValue(model, value);
79
- } finally {
80
- reconciling.current = false;
81
- }
82
- }
83
- function MonacoDiffEditor({ original, modified, onChange, language = "javascript", dark = false, locale, readOnly = false, collapseUnchanged = false, lineWrapping = false, showLineNumbers = true, fontSize, lineHeight, height = "100%", width, className, options, hostOptions, loading = "Loading editor…", failure = "Editor failed to load", beforeMount, onMount }) {
84
- const hostState = useMonacoHost(locale, hostOptions);
85
- const { state: shikiState, highlighterRef } = useShikiHighlighter();
86
- const beforeMountRef = useLatest(beforeMount);
87
- const onMountRef = useLatest(onMount);
88
- const onChangeRef = useLatest(onChange);
89
- const originalRef = useLatest(original);
90
- const modifiedRef = useLatest(modified);
91
- const diffEditorRef = useRef(null);
92
- const reconcilingRef = useRef(false);
93
- const modifiedTransitionRef = useRef(false);
94
- const [reconcileRevision, requestControlledReconcile] = useControlledReconcileSignal();
95
- const initialValuesRef = useRef({
96
- original,
97
- modified
98
- });
99
- const mergedOptions = useMemo(() => {
100
- return {
101
- ...DEFAULT_MONACO_OPTIONS,
102
- ...diffDocumentOptions({
103
- lineWrapping,
104
- showLineNumbers,
105
- readOnly,
106
- collapseUnchanged,
107
- fontSize,
108
- lineHeight
109
- }),
110
- ...options
111
- };
112
- }, [
113
- lineWrapping,
114
- showLineNumbers,
115
- readOnly,
116
- collapseUnchanged,
117
- fontSize,
118
- lineHeight,
119
- options
120
- ]);
121
- useLayoutEffect(() => {
122
- reconcilePane(diffEditorRef.current?.getOriginalEditor().getModel() ?? null, original, reconcilingRef);
123
- }, [original]);
124
- useInsertionEffect(() => {
125
- const model = diffEditorRef.current?.getModifiedEditor().getModel();
126
- modifiedTransitionRef.current = model !== null && model !== void 0 && !modelValueMatches(model, modified);
127
- }, [modified, reconcileRevision]);
128
- useLayoutEffect(() => {
129
- try {
130
- reconcilePane(diffEditorRef.current?.getModifiedEditor().getModel() ?? null, modified, reconcilingRef);
131
- } finally {
132
- modifiedTransitionRef.current = false;
133
- }
134
- }, [modified, reconcileRevision]);
135
- if (hostState === "failed") return /* @__PURE__ */ jsx(EditorFace, { children: failure });
136
- if (hostState === "loading" || shikiState === "loading") return /* @__PURE__ */ jsx(EditorFace, { children: loading });
137
- const handleBeforeMount = (monaco) => {
138
- const highlighter = highlighterRef.current;
139
- if (highlighter) registerShikiHighlighting(monaco, highlighter);
140
- beforeMountRef.current?.(monaco);
141
- };
142
- const handleMount = (diffEditor, monaco) => {
143
- initialValuesRef.current = {
144
- original: originalRef.current,
145
- modified: modifiedRef.current
146
- };
147
- diffEditorRef.current = diffEditor;
148
- const modifiedEditor = diffEditor.getModifiedEditor();
149
- modifiedEditor.onDidChangeModelContent(() => {
150
- if (reconcilingRef.current || modifiedTransitionRef.current) return;
151
- onChangeRef.current?.(modifiedEditor.getValue());
152
- requestControlledReconcile();
153
- });
154
- reconcilePane(diffEditor.getOriginalEditor().getModel(), originalRef.current, reconcilingRef);
155
- reconcilePane(modifiedEditor.getModel(), modifiedRef.current, reconcilingRef);
156
- onMountRef.current?.(diffEditor, monaco);
157
- };
158
- const theme = shikiState === "ready" ? shikiThemeId(dark) : dark ? "vs-dark" : "vs";
159
- return /* @__PURE__ */ jsx(DiffEditor, {
160
- beforeMount: handleBeforeMount,
161
- className,
162
- height,
163
- language,
164
- loading,
165
- modified: diffEditorRef.current ? initialValuesRef.current.modified : modified,
166
- options: mergedOptions,
167
- original: diffEditorRef.current ? initialValuesRef.current.original : original,
168
- theme,
169
- width,
170
- onMount: handleMount
171
- });
172
- }
173
- //#endregion
174
62
  //#region src/monaco-editor.tsx
175
63
  function documentOptions({ placeholder, lineWrapping, showLineNumbers, folding, disableContextMenu, fontSize, lineHeight, tabSize }) {
176
64
  const resolved = {
177
65
  placeholder,
178
66
  wordWrap: lineWrapping ? "on" : "off",
67
+ wordWrapIndicator: lineWrapping,
179
68
  lineNumbers: showLineNumbers ? "on" : "off",
180
69
  folding,
181
70
  contextmenu: !disableContextMenu
@@ -310,7 +199,7 @@ function MonacoEditor({ value, onChange, language = "javascript", dark = false,
310
199
  return /* @__PURE__ */ jsx(Editor, {
311
200
  keepCurrentModel: true,
312
201
  beforeMount: handleBeforeMount,
313
- className,
202
+ className: className === void 0 ? "monaco-component" : `monaco-component ${className}`,
314
203
  defaultValue: value,
315
204
  height,
316
205
  language,
@@ -324,4 +213,238 @@ function MonacoEditor({ value, onChange, language = "javascript", dark = false,
324
213
  });
325
214
  }
326
215
  //#endregion
327
- export { MonacoDiffEditor as i, monacoLanguages as n, MonacoEditor as r, DEFAULT_MONACO_OPTIONS$1 as t };
216
+ //#region src/monaco-console-editor.tsx
217
+ const EDITOR_ID_KEY = Symbol.for("coldsmirk.inkstone.react.consoleEditorId");
218
+ function nextEditorId() {
219
+ const holder = globalThis;
220
+ const id = holder[EDITOR_ID_KEY] ?? 0;
221
+ holder[EDITOR_ID_KEY] = id + 1;
222
+ return id;
223
+ }
224
+ function caretOf(instance) {
225
+ const model = instance.getModel();
226
+ const selection = instance.getSelection();
227
+ if (model === null || selection === null) return {
228
+ text: "",
229
+ from: 0,
230
+ to: 0
231
+ };
232
+ return {
233
+ text: model.getValue(),
234
+ from: model.getOffsetAt(selection.getStartPosition()),
235
+ to: model.getOffsetAt(selection.getEndPosition())
236
+ };
237
+ }
238
+ function monacoRange(model, span) {
239
+ const from = model.getPositionAt(span.from);
240
+ const to = model.getPositionAt(span.to);
241
+ return {
242
+ startLineNumber: from.lineNumber,
243
+ startColumn: from.column,
244
+ endLineNumber: to.lineNumber,
245
+ endColumn: to.column
246
+ };
247
+ }
248
+ function markRunnable(instance, marked, span, className) {
249
+ const model = instance.getModel();
250
+ if (model === null) return;
251
+ const decorations = span === null ? [] : [{
252
+ range: monacoRange(model, span),
253
+ options: {
254
+ linesDecorationsClassName: className,
255
+ isWholeLine: true
256
+ }
257
+ }];
258
+ if (marked.current !== null) marked.current.set(decorations);
259
+ else if (decorations.length > 0) marked.current = instance.createDecorationsCollection(decorations);
260
+ }
261
+ function MonacoConsoleEditor({ value, onChange, dark = false, height = 200, fontSize, locale, placeholder, ariaLabel, loading, failure, onRun, runActionLabel = "Run", onCaret, onReady, onMount, options, language, namespace, extension, context, beforeMount, register, highlight = null, highlightClassName = `${namespace}-runnable` }) {
262
+ const [path] = useState(() => `file:///${namespace}/${String(nextEditorId())}.${extension}`);
263
+ const bindings = useRef([]);
264
+ const contextRef = useLatest(context);
265
+ const runRef = useLatest(onRun);
266
+ const caretRef = useLatest(onCaret);
267
+ const instance = useRef(null);
268
+ const marked = useRef(null);
269
+ useEffect(() => {
270
+ if (instance.current !== null) markRunnable(instance.current, marked, highlight, highlightClassName);
271
+ }, [highlight, highlightClassName]);
272
+ useEffect(() => () => {
273
+ for (const binding of bindings.current) binding.dispose();
274
+ bindings.current = [];
275
+ }, []);
276
+ const constructionOptions = useMemo(() => {
277
+ return {
278
+ ...options,
279
+ ariaLabel
280
+ };
281
+ }, [options, ariaLabel]);
282
+ const fill = height === "fill";
283
+ return /* @__PURE__ */ jsx("div", {
284
+ style: fill ? {
285
+ height: "100%",
286
+ minHeight: 0
287
+ } : { height },
288
+ children: /* @__PURE__ */ jsx(MonacoEditor, {
289
+ showLineNumbers: true,
290
+ beforeMount,
291
+ dark,
292
+ failure,
293
+ fontSize,
294
+ height: fill ? "100%" : height,
295
+ language,
296
+ loading,
297
+ locale,
298
+ options: constructionOptions,
299
+ path,
300
+ placeholder,
301
+ value,
302
+ onChange,
303
+ onMount: (live, monaco) => {
304
+ instance.current = live;
305
+ bindings.current.push(live.onDidChangeCursorSelection(() => caretRef.current?.(caretOf(live))), live.addAction({
306
+ id: `${namespace}.run`,
307
+ label: runActionLabel,
308
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
309
+ run: (current) => runRef.current?.(caretOf(current))
310
+ }), ...register?.({
311
+ monaco,
312
+ editor: live,
313
+ path,
314
+ context: () => contextRef.current
315
+ }) ?? []);
316
+ markRunnable(live, marked, highlight, highlightClassName);
317
+ onReady?.({
318
+ insert: (text) => {
319
+ const selection = live.getSelection();
320
+ if (selection) live.executeEdits(`${namespace}-insert`, [{
321
+ range: selection,
322
+ text,
323
+ forceMoveMarkers: true
324
+ }]);
325
+ live.focus();
326
+ },
327
+ focus: () => {
328
+ live.focus();
329
+ }
330
+ });
331
+ onMount?.(live, monaco);
332
+ }
333
+ })
334
+ });
335
+ }
336
+ //#endregion
337
+ //#region src/monaco-diff-editor.tsx
338
+ function diffDocumentOptions({ lineWrapping, showLineNumbers, readOnly, collapseUnchanged, fontSize, lineHeight }) {
339
+ const resolved = {
340
+ wordWrap: lineWrapping ? "on" : "off",
341
+ wordWrapIndicator: lineWrapping,
342
+ lineNumbers: showLineNumbers ? "on" : "off",
343
+ readOnly,
344
+ hideUnchangedRegions: { enabled: collapseUnchanged }
345
+ };
346
+ if (fontSize !== void 0) resolved.fontSize = fontSize;
347
+ if (lineHeight !== void 0) resolved.lineHeight = lineHeight;
348
+ return resolved;
349
+ }
350
+ function reconcilePane(model, value, reconciling) {
351
+ if (!model) return;
352
+ reconciling.current = true;
353
+ try {
354
+ reconcileModelValue(model, value);
355
+ } finally {
356
+ reconciling.current = false;
357
+ }
358
+ }
359
+ function MonacoDiffEditor({ original, modified, onChange, language = "javascript", dark = false, locale, readOnly = false, collapseUnchanged = false, lineWrapping = false, showLineNumbers = true, fontSize, lineHeight, height = "100%", width, className, options, hostOptions, loading = "Loading editor…", failure = "Editor failed to load", beforeMount, onMount }) {
360
+ const hostState = useMonacoHost(locale, hostOptions);
361
+ const { state: shikiState, highlighterRef } = useShikiHighlighter();
362
+ const beforeMountRef = useLatest(beforeMount);
363
+ const onMountRef = useLatest(onMount);
364
+ const onChangeRef = useLatest(onChange);
365
+ const originalRef = useLatest(original);
366
+ const modifiedRef = useLatest(modified);
367
+ const diffEditorRef = useRef(null);
368
+ const reconcilingRef = useRef(false);
369
+ const modifiedTransitionRef = useRef(false);
370
+ const [reconcileRevision, requestControlledReconcile] = useControlledReconcileSignal();
371
+ const initialValuesRef = useRef({
372
+ original,
373
+ modified
374
+ });
375
+ const mergedOptions = useMemo(() => {
376
+ return {
377
+ ...DEFAULT_MONACO_OPTIONS,
378
+ ...diffDocumentOptions({
379
+ lineWrapping,
380
+ showLineNumbers,
381
+ readOnly,
382
+ collapseUnchanged,
383
+ fontSize,
384
+ lineHeight
385
+ }),
386
+ ...options
387
+ };
388
+ }, [
389
+ lineWrapping,
390
+ showLineNumbers,
391
+ readOnly,
392
+ collapseUnchanged,
393
+ fontSize,
394
+ lineHeight,
395
+ options
396
+ ]);
397
+ useLayoutEffect(() => {
398
+ reconcilePane(diffEditorRef.current?.getOriginalEditor().getModel() ?? null, original, reconcilingRef);
399
+ }, [original]);
400
+ useInsertionEffect(() => {
401
+ const model = diffEditorRef.current?.getModifiedEditor().getModel();
402
+ modifiedTransitionRef.current = model !== null && model !== void 0 && !modelValueMatches(model, modified);
403
+ }, [modified, reconcileRevision]);
404
+ useLayoutEffect(() => {
405
+ try {
406
+ reconcilePane(diffEditorRef.current?.getModifiedEditor().getModel() ?? null, modified, reconcilingRef);
407
+ } finally {
408
+ modifiedTransitionRef.current = false;
409
+ }
410
+ }, [modified, reconcileRevision]);
411
+ if (hostState === "failed") return /* @__PURE__ */ jsx(EditorFace, { children: failure });
412
+ if (hostState === "loading" || shikiState === "loading") return /* @__PURE__ */ jsx(EditorFace, { children: loading });
413
+ const handleBeforeMount = (monaco) => {
414
+ const highlighter = highlighterRef.current;
415
+ if (highlighter) registerShikiHighlighting(monaco, highlighter);
416
+ beforeMountRef.current?.(monaco);
417
+ };
418
+ const handleMount = (diffEditor, monaco) => {
419
+ initialValuesRef.current = {
420
+ original: originalRef.current,
421
+ modified: modifiedRef.current
422
+ };
423
+ diffEditorRef.current = diffEditor;
424
+ const modifiedEditor = diffEditor.getModifiedEditor();
425
+ modifiedEditor.onDidChangeModelContent(() => {
426
+ if (reconcilingRef.current || modifiedTransitionRef.current) return;
427
+ onChangeRef.current?.(modifiedEditor.getValue());
428
+ requestControlledReconcile();
429
+ });
430
+ reconcilePane(diffEditor.getOriginalEditor().getModel(), originalRef.current, reconcilingRef);
431
+ reconcilePane(modifiedEditor.getModel(), modifiedRef.current, reconcilingRef);
432
+ onMountRef.current?.(diffEditor, monaco);
433
+ };
434
+ const theme = shikiState === "ready" ? shikiThemeId(dark) : dark ? "vs-dark" : "vs";
435
+ return /* @__PURE__ */ jsx(DiffEditor, {
436
+ beforeMount: handleBeforeMount,
437
+ className,
438
+ height,
439
+ language,
440
+ loading,
441
+ modified: diffEditorRef.current ? initialValuesRef.current.modified : modified,
442
+ options: mergedOptions,
443
+ original: diffEditorRef.current ? initialValuesRef.current.original : original,
444
+ theme,
445
+ width,
446
+ onMount: handleMount
447
+ });
448
+ }
449
+ //#endregion
450
+ export { MonacoEditor as a, MonacoConsoleEditor as i, monacoLanguages as n, MonacoDiffEditor as r, DEFAULT_MONACO_OPTIONS$1 as t };
@@ -1,7 +1,191 @@
1
1
  import { ReactNode } from "react";
2
- import { DEFAULT_MONACO_OPTIONS as DEFAULT_MONACO_OPTIONS$1, MonacoHostOptions, MonacoLanguage, MonacoLanguage as MonacoLanguage$1, MonacoUiLocale, monacoLanguages } from "@coldsmirk/inkstone-monaco";
2
+ import { DEFAULT_MONACO_OPTIONS as DEFAULT_MONACO_OPTIONS$1, MonacoHostOptions, MonacoLanguage, MonacoLanguage as MonacoLanguage$1, MonacoModule, MonacoUiLocale, monacoLanguages } from "@coldsmirk/inkstone-monaco";
3
3
  import { BeforeMount, DiffBeforeMount, DiffOnMount, OnMount } from "@monaco-editor/react";
4
- import { editor } from "monaco-editor";
4
+ import { IDisposable, editor } from "monaco-editor";
5
+ //#region src/monaco-console-editor.d.ts
6
+ /**
7
+ * Where the operator is in the document, in offsets — everything a host needs to decide what a
8
+ * run should send, and nothing about the language. `from` and `to` are the selection's own
9
+ * bounds and are equal where nothing is selected: a caret is a selection of nothing.
10
+ *
11
+ * It rides with the text rather than beside it because the two must belong to the same instant.
12
+ * The controlled `value` is one React render behind the keystroke that triggered a run, and a
13
+ * console must run what the operator is looking at.
14
+ */
15
+ interface ConsoleCaret {
16
+ text: string;
17
+ from: number;
18
+ to: number;
19
+ }
20
+ /**
21
+ * What a host reaches into a console editor for: inserting at the caret — a schema, key or
22
+ * collection tree is an input device, not a browser — and the focus.
23
+ */
24
+ interface ConsoleEditorHandle {
25
+ insert: (text: string) => void;
26
+ focus: () => void;
27
+ }
28
+ /**
29
+ * What every console editor takes: `<MonacoConsoleEditor>`, and each language's editor built on
30
+ * it (`<SqlEditor>`, `<RedisEditor>`, `<MongodbEditor>`, `<ElasticsearchEditor>`).
31
+ */
32
+ interface ConsoleEditorProps<Handle extends ConsoleEditorHandle = ConsoleEditorHandle> {
33
+ value: string;
34
+ onChange: (value: string) => void;
35
+ /**
36
+ * Render with the dark theme. Like every inkstone editor, a console never reads the OS
37
+ * `prefers-color-scheme`; wire this to the app's color-scheme state.
38
+ *
39
+ * @default false
40
+ */
41
+ dark?: boolean;
42
+ /**
43
+ * Editor height in px, or `"fill"` to consume the parent's resolved height (the parent must be
44
+ * a `min-height: 0` flex item). Monaco's loading placeholder is `height: 100%`, so a height
45
+ * that resolves to nothing collapses the field to one line while the editor loads.
46
+ *
47
+ * @default 200
48
+ */
49
+ height?: number | "fill";
50
+ /**
51
+ * Editor font size in px. Omit to keep the inkstone default.
52
+ */
53
+ fontSize?: number;
54
+ /**
55
+ * UI locale for Monaco's built-in chrome — page-global and locked by the first editor to
56
+ * mount, exactly as on `<MonacoEditor>`.
57
+ */
58
+ locale?: MonacoUiLocale;
59
+ placeholder?: string;
60
+ /**
61
+ * The editor's accessible name, set on Monaco's own input surface — several consoles on one
62
+ * page are indistinguishable to assistive tech without one.
63
+ */
64
+ ariaLabel: string;
65
+ /**
66
+ * Shown while the Monaco host loads / when it fails to load — passed through to
67
+ * `<MonacoEditor>`, whose English defaults apply when omitted.
68
+ */
69
+ loading?: ReactNode;
70
+ failure?: ReactNode;
71
+ /**
72
+ * Ctrl/Cmd-Enter — the run key, bound inside the editor because that is where the operator's
73
+ * hands are. It is handed the editor's live caret, from which the host works out what to send.
74
+ * Read at every press, so it always runs against the connection the *current* render is bound
75
+ * to.
76
+ */
77
+ onRun?: (caret: ConsoleCaret) => void;
78
+ /**
79
+ * The label the run action shows in Monaco's own chrome (command palette, context menu).
80
+ */
81
+ runActionLabel?: string;
82
+ /**
83
+ * The caret moved, or the text under it changed. The host tracks it to show what a run would
84
+ * send; the run key does not depend on that tracking having caught up.
85
+ */
86
+ onCaret?: (caret: ConsoleCaret) => void;
87
+ onReady?: (handle: Handle) => void;
88
+ /**
89
+ * Runs after the editor's own mount wiring, with the live editor and the monaco module — the
90
+ * escape hatch for host-specific concerns (focus shields, extra actions).
91
+ */
92
+ onMount?: (instance: editor.IStandaloneCodeEditor, monaco: MonacoModule) => void;
93
+ /**
94
+ * Extra construction options, merged under the editor's own (`ariaLabel`): the escape hatch a
95
+ * host reaches for a construction option no prop names — `overflowWidgetsDomNode`, where the
96
+ * page's own layers would otherwise stand over the editor's floating widgets (hover,
97
+ * completion, parameter hints) left inside it. Merged on its identity, so keep it stable: a
98
+ * new object is applied to the live editor again.
99
+ */
100
+ options?: editor.IStandaloneEditorConstructionOptions;
101
+ }
102
+ /**
103
+ * What a console editor hands its language as the editor mounts.
104
+ */
105
+ interface ConsoleRegistration<Context> {
106
+ monaco: MonacoModule;
107
+ editor: editor.IStandaloneCodeEditor;
108
+ /**
109
+ * The model's path: what a registration that answers for one model compares a request's
110
+ * model against.
111
+ */
112
+ path: string;
113
+ /**
114
+ * The `context` prop as the latest committed render passed it. A registration happens once,
115
+ * at mount, so whatever it reaches that can change — an assist, a wording — is read through
116
+ * this at the moment it is needed and never captured: a completion after a connection switch
117
+ * can never answer from the connection a previous render was bound to.
118
+ */
119
+ context: () => Context;
120
+ }
121
+ interface MonacoConsoleEditorProps<Context> extends ConsoleEditorProps {
122
+ /**
123
+ * The Monaco language id the model is registered under.
124
+ */
125
+ language: string;
126
+ /**
127
+ * The name everything this editor puts into Monaco goes under: its model's directory
128
+ * (`file:///<namespace>/<n>.<extension>`), its run action (`<namespace>.run`), the source of
129
+ * its inserts (`<namespace>-insert`) and the runnable mark's default class
130
+ * (`<namespace>-runnable`).
131
+ */
132
+ namespace: string;
133
+ /**
134
+ * The extension the model's path ends in.
135
+ */
136
+ extension: string;
137
+ /**
138
+ * Whatever the language's registrations read that can change between renders, handed to
139
+ * `register` as a getter (`ConsoleRegistration.context`) — `undefined` where they read
140
+ * nothing.
141
+ */
142
+ context: Context;
143
+ /**
144
+ * Runs before the editor is created, after inkstone's own pre-mount setup: where a language
145
+ * defines its grammar.
146
+ */
147
+ beforeMount?: (monaco: MonacoModule) => void;
148
+ /**
149
+ * What the language registers for this editor — providers, markers, keybindings — once, as it
150
+ * mounts. The disposables go with the editor: Monaco's registry outlives it, and a provider
151
+ * left behind would answer for a model that no longer exists.
152
+ */
153
+ register?: (registration: ConsoleRegistration<Context>) => IDisposable[];
154
+ /**
155
+ * The label the run action shows in Monaco's own chrome (command palette, context menu).
156
+ *
157
+ * @default "Run"
158
+ */
159
+ runActionLabel?: string;
160
+ /**
161
+ * The span to mark as the one a run would send. Null leaves the gutter clean, which is the
162
+ * right answer while there is nothing to choose between.
163
+ */
164
+ highlight?: {
165
+ from: number;
166
+ to: number;
167
+ } | null;
168
+ /**
169
+ * The class the mark's line decoration carries. The host owns its stylesheet — a left-margin
170
+ * bar reads well, because it says which span without touching a single colour the syntax
171
+ * already assigned.
172
+ *
173
+ * @default `${namespace}-runnable`
174
+ */
175
+ highlightClassName?: string;
176
+ }
177
+ /**
178
+ * A console's editing surface: Monaco under the caller's language, the run key handing the live
179
+ * caret over, the caret reported as it moves, a handle that inserts at the caret and focuses, an
180
+ * optional mark on the span a run would send, and the language's own registrations made once as
181
+ * the editor mounts and disposed with it. The language editors of `@coldsmirk/inkstone-sql`,
182
+ * `-redis`, `-mongodb` and `-elasticsearch` are this component and their language's wiring.
183
+ *
184
+ * The editor draws no chrome of its own: it is a band of its host's surface, and the separation
185
+ * is the host's hairline rules — a border here would dress a pane up as a form input.
186
+ */
187
+ declare function MonacoConsoleEditor<Context>({ value, onChange, dark, height, fontSize, locale, placeholder, ariaLabel, loading, failure, onRun, runActionLabel, onCaret, onReady, onMount, options, language, namespace, extension, context, beforeMount, register, highlight, highlightClassName }: MonacoConsoleEditorProps<Context>): ReactNode;
188
+ //#endregion
5
189
  //#region src/monaco-diff-editor.d.ts
6
190
  interface MonacoDiffEditorProps {
7
191
  /**
@@ -63,7 +247,7 @@ interface MonacoDiffEditorProps {
63
247
  collapseUnchanged?: boolean;
64
248
  /**
65
249
  * Soft-wrap long lines on both sides (Monaco `wordWrap`; the diff editor's own
66
- * `diffWordWrap` inherits it).
250
+ * `diffWordWrap` inherits it), each wrapped line ending in Monaco's ↩ (`wordWrapIndicator`).
67
251
  *
68
252
  * @default false
69
253
  */
@@ -175,7 +359,8 @@ interface MonacoEditorProps {
175
359
  */
176
360
  placeholder?: string;
177
361
  /**
178
- * Soft-wrap long lines (Monaco `wordWrap`).
362
+ * Soft-wrap long lines (Monaco `wordWrap`), each wrapped line ending in Monaco's ↩
363
+ * (`wordWrapIndicator`), so a soft wrap reads apart from a real line break.
179
364
  *
180
365
  * @default false
181
366
  */
@@ -303,4 +488,4 @@ interface MonacoEditorProps {
303
488
  */
304
489
  declare function MonacoEditor({ value, onChange, language, dark, locale, placeholder, lineWrapping, showLineNumbers, folding, disableContextMenu, disableFind, disableCommandPalette, fontSize, lineHeight, tabSize, path, height, width, className, options, hostOptions, loading, failure, beforeMount, onMount, onFocus, onBlur }: MonacoEditorProps): ReactNode;
305
490
  //#endregion
306
- export { MonacoEditorProps as a, MonacoEditor as i, MonacoLanguage$1 as n, MonacoDiffEditor as o, monacoLanguages as r, MonacoDiffEditorProps as s, DEFAULT_MONACO_OPTIONS$1 as t };
491
+ export { MonacoEditorProps as a, ConsoleCaret as c, ConsoleRegistration as d, MonacoConsoleEditor as f, MonacoEditor as i, ConsoleEditorHandle as l, MonacoLanguage$1 as n, MonacoDiffEditor as o, MonacoConsoleEditorProps as p, monacoLanguages as r, MonacoDiffEditorProps as s, DEFAULT_MONACO_OPTIONS$1 as t, ConsoleEditorProps as u };
package/dist/monaco.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as MonacoEditorProps, i as MonacoEditor, n as MonacoLanguage, o as MonacoDiffEditor, r as monacoLanguages, s as MonacoDiffEditorProps, t as DEFAULT_MONACO_OPTIONS } from "./monaco-OhCg_705.js";
2
- export { DEFAULT_MONACO_OPTIONS, MonacoDiffEditor, type MonacoDiffEditorProps, MonacoEditor, type MonacoEditorProps, type MonacoLanguage, monacoLanguages };
1
+ import { a as MonacoEditorProps, c as ConsoleCaret, d as ConsoleRegistration, f as MonacoConsoleEditor, i as MonacoEditor, l as ConsoleEditorHandle, n as MonacoLanguage, o as MonacoDiffEditor, p as MonacoConsoleEditorProps, r as monacoLanguages, s as MonacoDiffEditorProps, t as DEFAULT_MONACO_OPTIONS, u as ConsoleEditorProps } from "./monaco-DTLlEEfk.js";
2
+ export { type ConsoleCaret, type ConsoleEditorHandle, type ConsoleEditorProps, type ConsoleRegistration, DEFAULT_MONACO_OPTIONS, MonacoConsoleEditor, type MonacoConsoleEditorProps, MonacoDiffEditor, type MonacoDiffEditorProps, MonacoEditor, type MonacoEditorProps, type MonacoLanguage, monacoLanguages };
package/dist/monaco.js CHANGED
@@ -1,2 +1 @@
1
- import { i as MonacoDiffEditor, n as monacoLanguages, r as MonacoEditor, t as DEFAULT_MONACO_OPTIONS } from "./monaco-FFb6p4ut.js";
2
- export { DEFAULT_MONACO_OPTIONS, MonacoDiffEditor, MonacoEditor, monacoLanguages };
1
+ export { t as DEFAULT_MONACO_OPTIONS, i as MonacoConsoleEditor, r as MonacoDiffEditor, a as MonacoEditor, n as monacoLanguages } from "./monaco-CHi4aRfZ.js";
package/dist/shiki.js CHANGED
@@ -1,2 +1 @@
1
- import { t as useShikiHighlighter } from "./use-shiki-highlighter-CzSQcX2f.js";
2
- export { useShikiHighlighter };
1
+ export { t as useShikiHighlighter } from "./use-shiki-highlighter-CzSQcX2f.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldsmirk/inkstone-react",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "Drop-in React code editors: a preconfigured Monaco <MonacoEditor> (offline workers, Shiki catppuccin themes, dark switching, opt-in zh-CN UI) and a controlled CodeMirror 6 <CodeMirrorEditor> (catppuccin themes, dark switching, sizing).",
5
5
  "keywords": [
6
6
  "react",
@@ -48,20 +48,20 @@
48
48
  ],
49
49
  "dependencies": {
50
50
  "@monaco-editor/react": "^4.7.0",
51
- "@coldsmirk/inkstone-core": "^0.30.0",
52
- "@coldsmirk/inkstone-monaco": "^0.30.0",
53
- "@coldsmirk/inkstone-codemirror": "^0.30.0"
51
+ "@coldsmirk/inkstone-codemirror": "^0.32.0",
52
+ "@coldsmirk/inkstone-core": "^0.32.0",
53
+ "@coldsmirk/inkstone-monaco": "^0.32.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@codemirror/autocomplete": "^6.20.3",
57
- "@codemirror/commands": "^6.10.4",
58
- "@codemirror/language": "^6.12.3",
59
- "@codemirror/search": "^6.6.0",
60
- "@codemirror/state": "^6.6.0",
61
- "@codemirror/view": "^6.43.0",
62
- "monaco-editor": "^0.56.0",
63
- "react": "^19.2.8",
64
- "react-dom": "^19.2.8"
57
+ "@codemirror/commands": "^6.11.1",
58
+ "@codemirror/language": "^6.13.1",
59
+ "@codemirror/search": "^6.7.2",
60
+ "@codemirror/state": "^6.7.6",
61
+ "@codemirror/view": "^6.43.14",
62
+ "monaco-editor": "^0.57.0",
63
+ "react": "^19.3.0",
64
+ "react-dom": "^19.3.0"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@codemirror/autocomplete": "^6.20.3",
@@ -70,7 +70,7 @@
70
70
  "@codemirror/search": "^6.6.0",
71
71
  "@codemirror/state": "^6.6.0",
72
72
  "@codemirror/view": "^6.43.0",
73
- "monaco-editor": "^0.56.0",
73
+ "monaco-editor": "^0.57.0",
74
74
  "react": ">=19"
75
75
  },
76
76
  "peerDependenciesMeta": {