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