@coldsmirk/inkstone-react 0.9.0 → 0.10.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.
@@ -0,0 +1,403 @@
1
+ import { n as useControlledReconcileSignal, r as CODE_FONT_FAMILY, t as useLatest } from "./use-latest-BhihMdOp.js";
2
+ import { t as useShikiHighlighter } from "./use-shiki-highlighter-CzSQcX2f.js";
3
+ import { useCallback, useEffect, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { shikiThemeId } from "@coldsmirk/inkstone-core";
6
+ import { DiffEditor, Editor, loader } from "@monaco-editor/react";
7
+ import { disabledKeybindings, ensureMonacoHost, installDisabledKeybindings, monacoLanguages } from "@coldsmirk/inkstone-monaco";
8
+ import { shikiToMonaco } from "@shikijs/monaco";
9
+ //#region src/editor-face.tsx
10
+ const FACE_STYLE = {
11
+ display: "flex",
12
+ alignItems: "center",
13
+ justifyContent: "center",
14
+ height: "100%",
15
+ width: "100%"
16
+ };
17
+ function EditorFace({ children }) {
18
+ return /* @__PURE__ */ jsx("div", {
19
+ style: FACE_STYLE,
20
+ children
21
+ });
22
+ }
23
+ //#endregion
24
+ //#region src/monaco-reconcile.ts
25
+ function reconcileModelValue(model, externalValue) {
26
+ const value = normalizeModelValue(model, externalValue);
27
+ const current = model.getValue();
28
+ if (value === current) return;
29
+ const shorter = Math.min(current.length, value.length);
30
+ let from = 0;
31
+ while (from < shorter && current.codePointAt(from) === value.codePointAt(from)) from += 1;
32
+ let currentEnd = current.length;
33
+ let valueEnd = value.length;
34
+ while (currentEnd > from && valueEnd > from && current.codePointAt(currentEnd - 1) === value.codePointAt(valueEnd - 1)) {
35
+ currentEnd -= 1;
36
+ valueEnd -= 1;
37
+ }
38
+ const start = model.getPositionAt(from);
39
+ const end = model.getPositionAt(currentEnd);
40
+ model.applyEdits([{
41
+ range: {
42
+ startLineNumber: start.lineNumber,
43
+ startColumn: start.column,
44
+ endLineNumber: end.lineNumber,
45
+ endColumn: end.column
46
+ },
47
+ text: value.slice(from, valueEnd)
48
+ }]);
49
+ }
50
+ function modelValueMatches(model, externalValue) {
51
+ return model.getValue() === normalizeModelValue(model, externalValue);
52
+ }
53
+ function normalizeModelValue(model, externalValue) {
54
+ return externalValue.split(/\r\n|\r|\n/).join(model.getEOL());
55
+ }
56
+ //#endregion
57
+ //#region src/use-monaco-host.ts
58
+ const BOOTSTRAP_KEY = Symbol.for("coldsmirk.inkstone.react.monacoBootstrap");
59
+ function bootstrapRegistry() {
60
+ const host = globalThis;
61
+ const existing = host[BOOTSTRAP_KEY];
62
+ if (existing?.configuredLoaders && existing.shikiMonacoModules) return existing;
63
+ const registry = {
64
+ configuredLoaders: /* @__PURE__ */ new WeakSet(),
65
+ shikiMonacoModules: /* @__PURE__ */ new WeakSet()
66
+ };
67
+ host[BOOTSTRAP_KEY] = registry;
68
+ return registry;
69
+ }
70
+ function configureMonacoLoader(targetLoader, monaco) {
71
+ const registry = bootstrapRegistry();
72
+ if (registry.configuredLoaders.has(targetLoader)) return;
73
+ targetLoader.config({ monaco });
74
+ registry.configuredLoaders.add(targetLoader);
75
+ }
76
+ function registerShikiHighlighting(monaco, highlighter) {
77
+ const registry = bootstrapRegistry();
78
+ if (registry.shikiMonacoModules.has(monaco)) return;
79
+ const canonical = new Set(highlighter.getLoadedLanguages().map((id) => highlighter.getLanguage(id).name));
80
+ const registered = new Set(monaco.languages.getLanguages().map((registeredLanguage) => registeredLanguage.id));
81
+ for (const lang of canonical) if (!registered.has(lang)) monaco.languages.register({ id: lang });
82
+ const shikiThemes = new Set(highlighter.getLoadedThemes());
83
+ const originalCreate = monaco.editor.create;
84
+ const originalSetTheme = monaco.editor.setTheme;
85
+ try {
86
+ shikiToMonaco(highlighter, monaco);
87
+ const setShikiTheme = monaco.editor.setTheme;
88
+ monaco.editor.setTheme = (themeName) => {
89
+ if (shikiThemes.has(themeName)) setShikiTheme.call(monaco.editor, themeName);
90
+ else originalSetTheme.call(monaco.editor, themeName);
91
+ };
92
+ } catch (error) {
93
+ monaco.editor.create = originalCreate;
94
+ monaco.editor.setTheme = originalSetTheme;
95
+ throw error;
96
+ }
97
+ registry.shikiMonacoModules.add(monaco);
98
+ }
99
+ function useMonacoHost(locale, hostOptions) {
100
+ const [state, setState] = useState("loading");
101
+ const hostOptionsRef = useRef(hostOptions);
102
+ const localeRef = useRef(locale);
103
+ useEffect(() => {
104
+ let cancelled = false;
105
+ ensureMonacoHost({
106
+ ...hostOptionsRef.current,
107
+ locale: localeRef.current ?? hostOptionsRef.current?.locale
108
+ }).then((monaco) => {
109
+ configureMonacoLoader(loader, monaco);
110
+ if (!cancelled) setState("ready");
111
+ }).catch((error) => {
112
+ console.error("inkstone: monaco host failed to load", error);
113
+ if (!cancelled) setState("failed");
114
+ });
115
+ return () => {
116
+ cancelled = true;
117
+ };
118
+ }, []);
119
+ return state;
120
+ }
121
+ //#endregion
122
+ //#region src/monaco-editor.tsx
123
+ const DEFAULT_MONACO_OPTIONS = {
124
+ find: { addExtraSpaceOnTop: false },
125
+ minimap: { enabled: false },
126
+ fontSize: 14,
127
+ lineHeight: 1.6,
128
+ fontFamily: CODE_FONT_FAMILY,
129
+ scrollBeyondLastLine: false,
130
+ automaticLayout: true,
131
+ renderLineHighlight: "line",
132
+ smoothScrolling: true,
133
+ padding: {
134
+ top: 12,
135
+ bottom: 12
136
+ },
137
+ fixedOverflowWidgets: true,
138
+ quickSuggestions: {
139
+ other: true,
140
+ comments: false,
141
+ strings: true
142
+ },
143
+ wordBasedSuggestions: "off",
144
+ tabCompletion: "on",
145
+ scrollbar: {
146
+ alwaysConsumeMouseWheel: false,
147
+ verticalScrollbarSize: 8,
148
+ horizontalScrollbarSize: 8
149
+ }
150
+ };
151
+ function documentOptions({ placeholder, lineWrapping, showLineNumbers, folding, disableContextMenu, fontSize, lineHeight, tabSize }) {
152
+ const resolved = {
153
+ placeholder,
154
+ wordWrap: lineWrapping ? "on" : "off",
155
+ lineNumbers: showLineNumbers ? "on" : "off",
156
+ folding,
157
+ contextmenu: !disableContextMenu
158
+ };
159
+ if (fontSize !== void 0) resolved.fontSize = fontSize;
160
+ if (lineHeight !== void 0) resolved.lineHeight = lineHeight;
161
+ if (tabSize !== void 0) resolved.tabSize = tabSize;
162
+ return resolved;
163
+ }
164
+ function reconcileModel(monaco, editorInstance, value, language, reconciling) {
165
+ const model = editorInstance.getModel();
166
+ if (!model) return;
167
+ reconciling.current = true;
168
+ try {
169
+ reconcileModelValue(model, value);
170
+ } finally {
171
+ reconciling.current = false;
172
+ }
173
+ if (model.getLanguageId() !== language) monaco.editor.setModelLanguage(model, language);
174
+ }
175
+ function MonacoEditor({ value, onChange, language = "javascript", dark = false, locale, placeholder, lineWrapping = false, showLineNumbers = false, folding = false, disableContextMenu = false, disableFind = false, disableCommandPalette = false, fontSize, lineHeight, tabSize = 2, path, height = "100%", width, className, options, hostOptions, loading = "Loading editor…", failure = "Editor failed to load", beforeMount, onMount, onFocus, onBlur }) {
176
+ const hostState = useMonacoHost(locale, hostOptions);
177
+ const { state: shikiState, highlighterRef } = useShikiHighlighter();
178
+ const beforeMountRef = useLatest(beforeMount);
179
+ const onMountRef = useLatest(onMount);
180
+ const onFocusRef = useLatest(onFocus);
181
+ const onBlurRef = useLatest(onBlur);
182
+ const onChangeRef = useLatest(onChange);
183
+ const valueRef = useLatest(value);
184
+ const languageRef = useLatest(language);
185
+ const editorRef = useRef(null);
186
+ const monacoRef = useRef(null);
187
+ const appliedPathRef = useRef(path);
188
+ const pathChangingRef = useRef(false);
189
+ const valueTransitionRef = useRef(false);
190
+ const reconcilingRef = useRef(false);
191
+ const [reconcileRevision, requestControlledReconcile] = useControlledReconcileSignal();
192
+ useInsertionEffect(() => {
193
+ pathChangingRef.current = path !== appliedPathRef.current;
194
+ }, [path]);
195
+ useInsertionEffect(() => {
196
+ const model = editorRef.current?.getModel();
197
+ valueTransitionRef.current = model !== null && model !== void 0 && !modelValueMatches(model, value);
198
+ }, [reconcileRevision, value]);
199
+ useLayoutEffect(() => {
200
+ if (pathChangingRef.current) return;
201
+ try {
202
+ const editorInstance = editorRef.current;
203
+ const monaco = monacoRef.current;
204
+ if (editorInstance && monaco) reconcileModel(monaco, editorInstance, value, languageRef.current, reconcilingRef);
205
+ } finally {
206
+ valueTransitionRef.current = false;
207
+ }
208
+ }, [
209
+ languageRef,
210
+ reconcileRevision,
211
+ value
212
+ ]);
213
+ useEffect(() => {
214
+ try {
215
+ const editorInstance = editorRef.current;
216
+ const monaco = monacoRef.current;
217
+ if (editorInstance && monaco) reconcileModel(monaco, editorInstance, valueRef.current, languageRef.current, reconcilingRef);
218
+ } finally {
219
+ appliedPathRef.current = path;
220
+ pathChangingRef.current = false;
221
+ valueTransitionRef.current = false;
222
+ }
223
+ }, [
224
+ languageRef,
225
+ path,
226
+ valueRef
227
+ ]);
228
+ const handleChange = useCallback((next) => {
229
+ if (pathChangingRef.current || valueTransitionRef.current || reconcilingRef.current) return;
230
+ onChangeRef.current?.(next ?? "");
231
+ requestControlledReconcile();
232
+ }, [onChangeRef, requestControlledReconcile]);
233
+ const mergedOptions = useMemo(() => {
234
+ return {
235
+ ...DEFAULT_MONACO_OPTIONS,
236
+ ...documentOptions({
237
+ placeholder,
238
+ lineWrapping,
239
+ showLineNumbers,
240
+ folding,
241
+ disableContextMenu,
242
+ fontSize,
243
+ lineHeight,
244
+ tabSize
245
+ }),
246
+ ...options
247
+ };
248
+ }, [
249
+ placeholder,
250
+ lineWrapping,
251
+ showLineNumbers,
252
+ folding,
253
+ disableContextMenu,
254
+ fontSize,
255
+ lineHeight,
256
+ tabSize,
257
+ options
258
+ ]);
259
+ if (hostState === "failed") return /* @__PURE__ */ jsx(EditorFace, { children: failure });
260
+ if (hostState === "loading" || shikiState === "loading") return /* @__PURE__ */ jsx(EditorFace, { children: loading });
261
+ const handleBeforeMount = (monaco) => {
262
+ const highlighter = highlighterRef.current;
263
+ if (highlighter) registerShikiHighlighting(monaco, highlighter);
264
+ beforeMountRef.current?.(monaco);
265
+ };
266
+ const handleMount = (editorInstance, monaco) => {
267
+ editorRef.current = editorInstance;
268
+ monacoRef.current = monaco;
269
+ reconcileModel(monaco, editorInstance, valueRef.current, languageRef.current, reconcilingRef);
270
+ installDisabledKeybindings(monaco, editorInstance, disabledKeybindings(monaco, disableFind, disableCommandPalette));
271
+ editorInstance.onDidFocusEditorText(() => onFocusRef.current?.());
272
+ editorInstance.onDidBlurEditorText(() => onBlurRef.current?.());
273
+ onMountRef.current?.(editorInstance, monaco);
274
+ };
275
+ return /* @__PURE__ */ jsx(Editor, {
276
+ beforeMount: handleBeforeMount,
277
+ className,
278
+ defaultValue: value,
279
+ height,
280
+ language,
281
+ loading,
282
+ options: mergedOptions,
283
+ path,
284
+ theme: shikiState === "ready" ? shikiThemeId(dark) : dark ? "vs-dark" : "vs",
285
+ width,
286
+ onChange: handleChange,
287
+ onMount: handleMount
288
+ });
289
+ }
290
+ //#endregion
291
+ //#region src/monaco-diff-editor.tsx
292
+ function diffDocumentOptions({ lineWrapping, showLineNumbers, readOnly, collapseUnchanged, fontSize, lineHeight }) {
293
+ const resolved = {
294
+ wordWrap: lineWrapping ? "on" : "off",
295
+ lineNumbers: showLineNumbers ? "on" : "off",
296
+ readOnly,
297
+ hideUnchangedRegions: { enabled: collapseUnchanged }
298
+ };
299
+ if (fontSize !== void 0) resolved.fontSize = fontSize;
300
+ if (lineHeight !== void 0) resolved.lineHeight = lineHeight;
301
+ return resolved;
302
+ }
303
+ function reconcilePane(model, value, reconciling) {
304
+ if (!model) return;
305
+ reconciling.current = true;
306
+ try {
307
+ reconcileModelValue(model, value);
308
+ } finally {
309
+ reconciling.current = false;
310
+ }
311
+ }
312
+ 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 }) {
313
+ const hostState = useMonacoHost(locale, hostOptions);
314
+ const { state: shikiState, highlighterRef } = useShikiHighlighter();
315
+ const beforeMountRef = useLatest(beforeMount);
316
+ const onMountRef = useLatest(onMount);
317
+ const onChangeRef = useLatest(onChange);
318
+ const originalRef = useLatest(original);
319
+ const modifiedRef = useLatest(modified);
320
+ const diffEditorRef = useRef(null);
321
+ const reconcilingRef = useRef(false);
322
+ const modifiedTransitionRef = useRef(false);
323
+ const [reconcileRevision, requestControlledReconcile] = useControlledReconcileSignal();
324
+ const initialValuesRef = useRef({
325
+ original,
326
+ modified
327
+ });
328
+ const mergedOptions = useMemo(() => {
329
+ return {
330
+ ...DEFAULT_MONACO_OPTIONS,
331
+ ...diffDocumentOptions({
332
+ lineWrapping,
333
+ showLineNumbers,
334
+ readOnly,
335
+ collapseUnchanged,
336
+ fontSize,
337
+ lineHeight
338
+ }),
339
+ ...options
340
+ };
341
+ }, [
342
+ lineWrapping,
343
+ showLineNumbers,
344
+ readOnly,
345
+ collapseUnchanged,
346
+ fontSize,
347
+ lineHeight,
348
+ options
349
+ ]);
350
+ useLayoutEffect(() => {
351
+ reconcilePane(diffEditorRef.current?.getOriginalEditor().getModel() ?? null, original, reconcilingRef);
352
+ }, [original]);
353
+ useInsertionEffect(() => {
354
+ const model = diffEditorRef.current?.getModifiedEditor().getModel();
355
+ modifiedTransitionRef.current = model !== null && model !== void 0 && !modelValueMatches(model, modified);
356
+ }, [modified, reconcileRevision]);
357
+ useLayoutEffect(() => {
358
+ try {
359
+ reconcilePane(diffEditorRef.current?.getModifiedEditor().getModel() ?? null, modified, reconcilingRef);
360
+ } finally {
361
+ modifiedTransitionRef.current = false;
362
+ }
363
+ }, [modified, reconcileRevision]);
364
+ if (hostState === "failed") return /* @__PURE__ */ jsx(EditorFace, { children: failure });
365
+ if (hostState === "loading" || shikiState === "loading") return /* @__PURE__ */ jsx(EditorFace, { children: loading });
366
+ const handleBeforeMount = (monaco) => {
367
+ const highlighter = highlighterRef.current;
368
+ if (highlighter) registerShikiHighlighting(monaco, highlighter);
369
+ beforeMountRef.current?.(monaco);
370
+ };
371
+ const handleMount = (diffEditor, monaco) => {
372
+ initialValuesRef.current = {
373
+ original: originalRef.current,
374
+ modified: modifiedRef.current
375
+ };
376
+ diffEditorRef.current = diffEditor;
377
+ const modifiedEditor = diffEditor.getModifiedEditor();
378
+ modifiedEditor.onDidChangeModelContent(() => {
379
+ if (reconcilingRef.current || modifiedTransitionRef.current) return;
380
+ onChangeRef.current?.(modifiedEditor.getValue());
381
+ requestControlledReconcile();
382
+ });
383
+ reconcilePane(diffEditor.getOriginalEditor().getModel(), originalRef.current, reconcilingRef);
384
+ reconcilePane(modifiedEditor.getModel(), modifiedRef.current, reconcilingRef);
385
+ onMountRef.current?.(diffEditor, monaco);
386
+ };
387
+ const theme = shikiState === "ready" ? shikiThemeId(dark) : dark ? "vs-dark" : "vs";
388
+ return /* @__PURE__ */ jsx(DiffEditor, {
389
+ beforeMount: handleBeforeMount,
390
+ className,
391
+ height,
392
+ language,
393
+ loading,
394
+ modified: diffEditorRef.current ? initialValuesRef.current.modified : modified,
395
+ options: mergedOptions,
396
+ original: diffEditorRef.current ? initialValuesRef.current.original : original,
397
+ theme,
398
+ width,
399
+ onMount: handleMount
400
+ });
401
+ }
402
+ //#endregion
403
+ export { MonacoEditor as i, MonacoDiffEditor as n, DEFAULT_MONACO_OPTIONS as r, monacoLanguages as t };