@octanejs/monaco-editor 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Suren Atoyan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @octanejs/monaco-editor
2
+
3
+ Monaco Editor components for Octane. This binding reuses `monaco-editor` and
4
+ `@monaco-editor/loader` unchanged and ports the `@monaco-editor/react@4.7.0`
5
+ adapter layer to compiled Octane components.
6
+
7
+ ```bash
8
+ pnpm add @octanejs/monaco-editor monaco-editor
9
+ ```
10
+
11
+ ```tsrx
12
+ import Editor, { loader } from '@octanejs/monaco-editor';
13
+ import * as monaco from 'monaco-editor';
14
+
15
+ loader.config({ monaco });
16
+
17
+ export function CodeEditor(props: { value: string; onChange(value: string): void }) @{
18
+ <Editor
19
+ height="400px"
20
+ language="typescript"
21
+ value={props.value}
22
+ onChange={(value) => props.onChange(value ?? '')}
23
+ />
24
+ }
25
+ ```
26
+
27
+ The root entry exports `Editor` (also the default export), `DiffEditor`,
28
+ `useMonaco`, `loader`, and the corresponding public Monaco-backed prop and
29
+ callback types. Models created by a component are disposed on unmount unless
30
+ the matching `keepCurrent*Model` option preserves the active model.
package/UPSTREAM.md ADDED
@@ -0,0 +1,10 @@
1
+ # Upstream
2
+
3
+ - Package: `@monaco-editor/react`
4
+ - Version: `4.7.0`
5
+ - Source: https://github.com/suren-atoyan/monaco-react
6
+ - License: MIT
7
+ - Loader: `@monaco-editor/loader@1.7.0`
8
+
9
+ The component and hook layer is re-authored for Octane. The framework-neutral
10
+ Monaco editor and loader packages are consumed unchanged.
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@octanejs/monaco-editor",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "engines": {
8
+ "node": ">=22.22.2"
9
+ },
10
+ "description": "Monaco Editor components and loader hooks for Octane, ported from @monaco-editor/react without a React runtime dependency.",
11
+ "author": {
12
+ "name": "Dominic Gannaway",
13
+ "email": "dg@domgan.com"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/octanejs/octane.git",
21
+ "directory": "packages/monaco-editor"
22
+ },
23
+ "main": "src/index.ts",
24
+ "module": "src/index.ts",
25
+ "types": "src/index.ts",
26
+ "files": [
27
+ "src",
28
+ "LICENSE",
29
+ "README.md",
30
+ "UPSTREAM.md"
31
+ ],
32
+ "exports": {
33
+ ".": "./src/index.ts"
34
+ },
35
+ "dependencies": {
36
+ "@monaco-editor/loader": "1.7.0"
37
+ },
38
+ "peerDependencies": {
39
+ "monaco-editor": ">=0.25.0 <1",
40
+ "octane": "0.1.33"
41
+ },
42
+ "devDependencies": {
43
+ "@monaco-editor/react": "4.7.0",
44
+ "@testing-library/jest-dom": "^6.9.1",
45
+ "monaco-editor": "0.55.1",
46
+ "vitest": "^4.1.10",
47
+ "octane": "0.1.33",
48
+ "@octanejs/testing-library": "0.1.30"
49
+ },
50
+ "scripts": {
51
+ "test": "vitest run --root ../.. --project monaco-editor",
52
+ "typecheck": "tsrx-tsc --noEmit -p tsconfig.json"
53
+ }
54
+ }
@@ -0,0 +1,43 @@
1
+ import type { OctaneNode } from 'octane';
2
+
3
+ import type { WrapperProps } from './types';
4
+
5
+ const LOADING_STYLE = {
6
+ alignItems: 'center',
7
+ display: 'flex',
8
+ height: '100%',
9
+ justifyContent: 'center',
10
+ width: '100%',
11
+ };
12
+
13
+ export interface ContainerProps {
14
+ width: number | string;
15
+ height: number | string;
16
+ isEditorReady: boolean;
17
+ loading: OctaneNode;
18
+ containerRef: { current: HTMLDivElement | null };
19
+ className?: string;
20
+ wrapperProps: WrapperProps;
21
+ }
22
+
23
+ export function Container(props: ContainerProps) @{
24
+ const { width, height, isEditorReady, loading, containerRef, className, wrapperProps } = props;
25
+ const wrapperStyle = {
26
+ display: 'flex' as const,
27
+ position: 'relative' as const,
28
+ textAlign: 'initial' as const,
29
+ width,
30
+ height,
31
+ };
32
+ const editorStyle = {
33
+ display: isEditorReady ? undefined : 'none' as const,
34
+ width: '100%',
35
+ };
36
+
37
+ <section style={wrapperStyle} {...wrapperProps}>
38
+ @if (!isEditorReady) {
39
+ <div style={LOADING_STYLE}>{loading}</div>
40
+ }
41
+ <div ref={containerRef} style={editorStyle} class={className} />
42
+ </section>
43
+ }
@@ -0,0 +1,16 @@
1
+ import type { OctaneNode } from 'octane';
2
+ import type { Octane } from 'octane/jsx-runtime';
3
+
4
+ import type { WrapperProps } from './types';
5
+
6
+ export interface ContainerProps {
7
+ width: number | string;
8
+ height: number | string;
9
+ isEditorReady: boolean;
10
+ loading: OctaneNode;
11
+ containerRef: { current: HTMLDivElement | null };
12
+ className?: string;
13
+ wrapperProps: WrapperProps;
14
+ }
15
+
16
+ export declare function Container(props: ContainerProps): Octane.JSX.Element;
@@ -0,0 +1,245 @@
1
+ import loader from '@monaco-editor/loader';
2
+ import type { editor } from 'monaco-editor';
3
+ import { memo, useEffect, useRef, useState } from 'octane';
4
+
5
+ import { Container } from './Container.tsrx';
6
+ import { isLoaderCancelation } from './loader';
7
+ import { getOrCreateModel, releaseModels } from './model';
8
+ import type { DiffEditorProps, Monaco, MonacoDiffEditor } from './types';
9
+
10
+ const noop = (): void => {};
11
+ const EMPTY_OPTIONS: editor.IDiffEditorConstructionOptions = {};
12
+ const EMPTY_WRAPPER_PROPS = {};
13
+
14
+ function DiffEditorComponent(props: DiffEditorProps) @{
15
+ const {
16
+ original,
17
+ modified,
18
+ language,
19
+ originalLanguage,
20
+ modifiedLanguage,
21
+ originalModelPath,
22
+ modifiedModelPath,
23
+ keepCurrentOriginalModel = false,
24
+ keepCurrentModifiedModel = false,
25
+ theme = 'light',
26
+ loading = 'Loading...',
27
+ options = EMPTY_OPTIONS,
28
+ height = '100%',
29
+ width = '100%',
30
+ className,
31
+ wrapperProps = EMPTY_WRAPPER_PROPS,
32
+ beforeMount = noop,
33
+ onMount = noop,
34
+ } = props;
35
+
36
+ const [monaco, setMonaco] = useState<Monaco | null>(null);
37
+ const [isEditorReady, setIsEditorReady] = useState(false);
38
+ const editorRef = useRef<MonacoDiffEditor | null>(null);
39
+ const containerRef = useRef<HTMLDivElement | null>(null);
40
+ const leasedModelsRef = useRef(new Set<editor.ITextModel>());
41
+ const currentOriginalPathRef = useRef<string | undefined>(originalModelPath);
42
+ const currentModifiedPathRef = useRef<string | undefined>(modifiedModelPath);
43
+ const latestRef = useRef({
44
+ beforeMount,
45
+ keepCurrentModifiedModel,
46
+ keepCurrentOriginalModel,
47
+ onMount,
48
+ });
49
+
50
+ latestRef.current = {
51
+ beforeMount,
52
+ keepCurrentModifiedModel,
53
+ keepCurrentOriginalModel,
54
+ onMount,
55
+ };
56
+
57
+ useEffect(() => {
58
+ let active = true;
59
+ const request = loader.init();
60
+ request.then((loadedMonaco) => {
61
+ if (active) setMonaco(loadedMonaco);
62
+ }).catch((error: unknown) => {
63
+ if (active && !isLoaderCancelation(error)) {
64
+ console.error('Monaco initialization: error:', error);
65
+ }
66
+ });
67
+
68
+ return () => {
69
+ active = false;
70
+ request.cancel();
71
+ };
72
+ }, []);
73
+
74
+ useEffect(() => {
75
+ if (!monaco || !containerRef.current || editorRef.current) return;
76
+
77
+ let createdEditor: MonacoDiffEditor | null = null;
78
+ try {
79
+ latestRef.current.beforeMount(monaco);
80
+ const originalModel = getOrCreateModel(
81
+ monaco,
82
+ original ?? '',
83
+ originalLanguage ?? language ?? 'text',
84
+ originalModelPath,
85
+ leasedModelsRef.current,
86
+ );
87
+ const modifiedModel = getOrCreateModel(
88
+ monaco,
89
+ modified ?? '',
90
+ modifiedLanguage ?? language ?? 'text',
91
+ modifiedModelPath,
92
+ leasedModelsRef.current,
93
+ );
94
+ const editorInstance = monaco.editor.createDiffEditor(containerRef.current, {
95
+ automaticLayout: true,
96
+ ...options,
97
+ });
98
+ createdEditor = editorInstance;
99
+ editorInstance.setModel({ original: originalModel, modified: modifiedModel });
100
+ editorRef.current = editorInstance;
101
+ currentOriginalPathRef.current = originalModelPath;
102
+ currentModifiedPathRef.current = modifiedModelPath;
103
+ monaco.editor.setTheme(theme);
104
+ setIsEditorReady(true);
105
+
106
+ return () => {
107
+ if (editorRef.current !== editorInstance) return;
108
+
109
+ const models = editorInstance.getModel();
110
+ editorInstance.dispose();
111
+ editorRef.current = null;
112
+
113
+ const preserved = new Set<editor.ITextModel>();
114
+ if (latestRef.current.keepCurrentOriginalModel && models?.original) {
115
+ preserved.add(models.original);
116
+ }
117
+ if (latestRef.current.keepCurrentModifiedModel && models?.modified) {
118
+ preserved.add(models.modified);
119
+ }
120
+ releaseModels(leasedModelsRef.current, preserved);
121
+ };
122
+ } catch (error) {
123
+ createdEditor?.dispose();
124
+ editorRef.current = null;
125
+ releaseModels(leasedModelsRef.current, new Set());
126
+ throw error;
127
+ }
128
+ }, [monaco]);
129
+
130
+ useEffect(() => {
131
+ const editorInstance = editorRef.current;
132
+ if (!isEditorReady || !editorInstance || !monaco) return;
133
+
134
+ // Reason: readiness must commit first so consumers can focus or measure a visible editor.
135
+ try {
136
+ latestRef.current.onMount(editorInstance, monaco);
137
+ } catch (error) {
138
+ editorInstance.dispose();
139
+ editorRef.current = null;
140
+ releaseModels(leasedModelsRef.current, new Set());
141
+ throw error;
142
+ }
143
+ }, [isEditorReady]);
144
+
145
+ useEffect(() => {
146
+ const editorInstance = editorRef.current;
147
+ const currentModels = editorInstance?.getModel();
148
+ if (!isEditorReady || !editorInstance || !currentModels || !monaco) return;
149
+
150
+ const nextOriginal =
151
+ originalModelPath === currentOriginalPathRef.current
152
+ ? currentModels.original
153
+ : getOrCreateModel(
154
+ monaco,
155
+ original ?? '',
156
+ originalLanguage ?? language ?? 'text',
157
+ originalModelPath,
158
+ leasedModelsRef.current,
159
+ );
160
+ const nextModified =
161
+ modifiedModelPath === currentModifiedPathRef.current
162
+ ? currentModels.modified
163
+ : getOrCreateModel(
164
+ monaco,
165
+ modified ?? '',
166
+ modifiedLanguage ?? language ?? 'text',
167
+ modifiedModelPath,
168
+ leasedModelsRef.current,
169
+ );
170
+
171
+ if (nextOriginal !== currentModels.original || nextModified !== currentModels.modified) {
172
+ editorInstance.setModel({
173
+ original: nextOriginal,
174
+ modified: nextModified,
175
+ });
176
+ }
177
+ currentOriginalPathRef.current = originalModelPath;
178
+ currentModifiedPathRef.current = modifiedModelPath;
179
+ }, [originalModelPath, modifiedModelPath]);
180
+
181
+ useEffect(() => {
182
+ if (isEditorReady) editorRef.current?.updateOptions(options);
183
+ }, [options]);
184
+
185
+ useEffect(() => {
186
+ const model = editorRef.current?.getModel()?.original;
187
+ const nextValue = original ?? '';
188
+ if (isEditorReady && model && model.getValue() !== nextValue) {
189
+ model.setValue(nextValue);
190
+ }
191
+ }, [isEditorReady, original, originalModelPath]);
192
+
193
+ useEffect(() => {
194
+ const editorInstance = editorRef.current?.getModifiedEditor();
195
+ const nextValue = modified ?? '';
196
+ if (!isEditorReady || !editorInstance || !monaco || editorInstance.getValue() === nextValue) {
197
+ return;
198
+ }
199
+
200
+ if (editorInstance.getOption(monaco.editor.EditorOption.readOnly)) {
201
+ editorInstance.setValue(nextValue);
202
+ } else {
203
+ const model = editorInstance.getModel();
204
+ if (!model) return;
205
+ editorInstance.executeEdits('', [
206
+ {
207
+ range: model.getFullModelRange(),
208
+ text: nextValue,
209
+ forceMoveMarkers: true,
210
+ },
211
+ ]);
212
+ editorInstance.pushUndoStop();
213
+ }
214
+ }, [isEditorReady, modified, modifiedModelPath]);
215
+
216
+ useEffect(() => {
217
+ const models = editorRef.current?.getModel();
218
+ if (!isEditorReady || !models || !monaco) return;
219
+ monaco.editor.setModelLanguage(models.original, originalLanguage ?? language ?? 'text');
220
+ monaco.editor.setModelLanguage(models.modified, modifiedLanguage ?? language ?? 'text');
221
+ }, [
222
+ isEditorReady,
223
+ language,
224
+ originalLanguage,
225
+ modifiedLanguage,
226
+ originalModelPath,
227
+ modifiedModelPath,
228
+ ]);
229
+
230
+ useEffect(() => {
231
+ if (isEditorReady && monaco) monaco.editor.setTheme(theme);
232
+ }, [theme]);
233
+
234
+ <Container
235
+ width={width}
236
+ height={height}
237
+ isEditorReady={isEditorReady}
238
+ loading={loading}
239
+ containerRef={containerRef}
240
+ className={className}
241
+ wrapperProps={wrapperProps}
242
+ />
243
+ }
244
+
245
+ export const DiffEditor = memo(DiffEditorComponent);
@@ -0,0 +1,5 @@
1
+ import type { Octane } from 'octane/jsx-runtime';
2
+
3
+ import type { DiffEditorProps } from './types';
4
+
5
+ export declare const DiffEditor: (props: DiffEditorProps) => Octane.JSX.Element;
@@ -0,0 +1,270 @@
1
+ import loader from '@monaco-editor/loader';
2
+ import type { IDisposable, editor } from 'monaco-editor';
3
+ import { memo, useEffect, useRef, useState } from 'octane';
4
+
5
+ import { Container } from './Container.tsrx';
6
+ import { isLoaderCancelation } from './loader';
7
+ import { getOrCreateModel, releaseModels } from './model';
8
+ import type { EditorProps, Monaco, MonacoEditor } from './types';
9
+
10
+ const noop = (): void => {};
11
+ const EMPTY_OPTIONS: editor.IStandaloneEditorConstructionOptions = {};
12
+ const EMPTY_OVERRIDE_SERVICES: editor.IEditorOverrideServices = {};
13
+ const EMPTY_WRAPPER_PROPS = {};
14
+ // Kept models outlive a component instance, so their view state must as well.
15
+ const modelViewStates = new WeakMap<editor.ITextModel, editor.ICodeEditorViewState | null>();
16
+
17
+ function EditorComponent(props: EditorProps) @{
18
+ const {
19
+ defaultValue,
20
+ defaultLanguage,
21
+ defaultPath,
22
+ value,
23
+ language,
24
+ path,
25
+ theme = 'light',
26
+ line,
27
+ loading = 'Loading...',
28
+ options = EMPTY_OPTIONS,
29
+ overrideServices = EMPTY_OVERRIDE_SERVICES,
30
+ saveViewState = true,
31
+ keepCurrentModel = false,
32
+ width = '100%',
33
+ height = '100%',
34
+ className,
35
+ wrapperProps = EMPTY_WRAPPER_PROPS,
36
+ beforeMount = noop,
37
+ onMount = noop,
38
+ onChange,
39
+ onValidate,
40
+ } = props;
41
+
42
+ const [monaco, setMonaco] = useState<Monaco | null>(null);
43
+ const [isEditorReady, setIsEditorReady] = useState(false);
44
+ const editorRef = useRef<MonacoEditor | null>(null);
45
+ const containerRef = useRef<HTMLDivElement | null>(null);
46
+ const contentSubscriptionRef = useRef<IDisposable | null>(null);
47
+ const markerSubscriptionRef = useRef<IDisposable | null>(null);
48
+ const leasedModelsRef = useRef(new Set<editor.ITextModel>());
49
+ const currentModelKeyRef = useRef<string | undefined>(path ?? defaultPath);
50
+ const preventChangeRef = useRef(false);
51
+ const latestRef = useRef({
52
+ beforeMount,
53
+ keepCurrentModel,
54
+ onChange,
55
+ onMount,
56
+ onValidate,
57
+ saveViewState,
58
+ });
59
+
60
+ latestRef.current = {
61
+ beforeMount,
62
+ keepCurrentModel,
63
+ onChange,
64
+ onMount,
65
+ onValidate,
66
+ saveViewState,
67
+ };
68
+
69
+ useEffect(() => {
70
+ let active = true;
71
+ const request = loader.init();
72
+
73
+ request.then((loadedMonaco) => {
74
+ if (active) setMonaco(loadedMonaco);
75
+ }).catch((error: unknown) => {
76
+ if (active && !isLoaderCancelation(error)) {
77
+ console.error('Monaco initialization: error:', error);
78
+ }
79
+ });
80
+
81
+ return () => {
82
+ active = false;
83
+ request.cancel();
84
+ };
85
+ }, []);
86
+
87
+ useEffect(() => {
88
+ if (!monaco || !containerRef.current || editorRef.current) return;
89
+
90
+ let createdEditor: MonacoEditor | null = null;
91
+ try {
92
+ latestRef.current.beforeMount(monaco);
93
+ const initialPath = path ?? defaultPath;
94
+ const initialModel = getOrCreateModel(
95
+ monaco,
96
+ value ?? defaultValue ?? '',
97
+ language ?? defaultLanguage ?? '',
98
+ initialPath,
99
+ leasedModelsRef.current,
100
+ );
101
+ const editorInstance = monaco.editor.create(
102
+ containerRef.current,
103
+ {
104
+ model: initialModel,
105
+ automaticLayout: true,
106
+ ...options,
107
+ },
108
+ overrideServices,
109
+ );
110
+ createdEditor = editorInstance;
111
+
112
+ editorRef.current = editorInstance;
113
+ currentModelKeyRef.current = initialPath;
114
+ const initialViewState = modelViewStates.get(initialModel);
115
+ if (saveViewState && initialViewState) {
116
+ editorInstance.restoreViewState(initialViewState);
117
+ }
118
+ monaco.editor.setTheme(theme);
119
+ if (line !== undefined) editorInstance.revealLine(line);
120
+
121
+ contentSubscriptionRef.current = editorInstance.onDidChangeModelContent((event) => {
122
+ if (!preventChangeRef.current) {
123
+ latestRef.current.onChange?.(editorInstance.getValue(), event);
124
+ }
125
+ });
126
+ markerSubscriptionRef.current = monaco.editor.onDidChangeMarkers((uris) => {
127
+ const editorUri = editorInstance.getModel()?.uri;
128
+ if (editorUri && uris.some((uri) => uri.toString() === editorUri.toString())) {
129
+ latestRef.current.onValidate?.(monaco.editor.getModelMarkers({ resource: editorUri }));
130
+ }
131
+ });
132
+ setIsEditorReady(true);
133
+
134
+ return () => {
135
+ if (editorRef.current !== editorInstance) return;
136
+
137
+ contentSubscriptionRef.current?.dispose();
138
+ markerSubscriptionRef.current?.dispose();
139
+ contentSubscriptionRef.current = null;
140
+ markerSubscriptionRef.current = null;
141
+
142
+ const currentModel = editorInstance.getModel();
143
+ const latest = latestRef.current;
144
+ if (latest.saveViewState && latest.keepCurrentModel && currentModel) {
145
+ modelViewStates.set(currentModel, editorInstance.saveViewState());
146
+ }
147
+
148
+ editorInstance.dispose();
149
+ editorRef.current = null;
150
+ const preserved = new Set<editor.ITextModel>();
151
+ if (latest.keepCurrentModel && currentModel) preserved.add(currentModel);
152
+ releaseModels(leasedModelsRef.current, preserved);
153
+ };
154
+ } catch (error) {
155
+ contentSubscriptionRef.current?.dispose();
156
+ markerSubscriptionRef.current?.dispose();
157
+ contentSubscriptionRef.current = null;
158
+ markerSubscriptionRef.current = null;
159
+ createdEditor?.dispose();
160
+ editorRef.current = null;
161
+ releaseModels(leasedModelsRef.current, new Set());
162
+ throw error;
163
+ }
164
+ }, [monaco]);
165
+
166
+ useEffect(() => {
167
+ const editorInstance = editorRef.current;
168
+ if (!isEditorReady || !editorInstance || !monaco) return;
169
+
170
+ // Reason: readiness must commit first so consumers can focus or measure a visible editor.
171
+ try {
172
+ latestRef.current.onMount(editorInstance, monaco);
173
+ } catch (error) {
174
+ contentSubscriptionRef.current?.dispose();
175
+ markerSubscriptionRef.current?.dispose();
176
+ contentSubscriptionRef.current = null;
177
+ markerSubscriptionRef.current = null;
178
+ editorInstance.dispose();
179
+ editorRef.current = null;
180
+ releaseModels(leasedModelsRef.current, new Set());
181
+ throw error;
182
+ }
183
+ }, [isEditorReady]);
184
+
185
+ useEffect(() => {
186
+ const editorInstance = editorRef.current;
187
+ if (!isEditorReady || !editorInstance || !monaco) return;
188
+
189
+ const nextModelKey = path ?? defaultPath;
190
+ if (nextModelKey === currentModelKeyRef.current) return;
191
+
192
+ const currentModel = editorInstance.getModel();
193
+ if (saveViewState && currentModel) {
194
+ modelViewStates.set(currentModel, editorInstance.saveViewState());
195
+ }
196
+ const nextModel = getOrCreateModel(
197
+ monaco,
198
+ value ?? defaultValue ?? '',
199
+ language ?? defaultLanguage ?? '',
200
+ nextModelKey,
201
+ leasedModelsRef.current,
202
+ );
203
+ if (nextModel !== currentModel) editorInstance.setModel(nextModel);
204
+ currentModelKeyRef.current = nextModelKey;
205
+ const nextViewState = modelViewStates.get(nextModel);
206
+ if (saveViewState && nextViewState) {
207
+ editorInstance.restoreViewState(nextViewState);
208
+ }
209
+ }, [path]);
210
+
211
+ useEffect(() => {
212
+ if (isEditorReady) editorRef.current?.updateOptions(options);
213
+ }, [options]);
214
+
215
+ useEffect(() => {
216
+ const editorInstance = editorRef.current;
217
+ if (!isEditorReady || !editorInstance || !monaco || value === undefined) return;
218
+ if (value === editorInstance.getValue()) return;
219
+
220
+ preventChangeRef.current = true;
221
+ try {
222
+ if (editorInstance.getOption(monaco.editor.EditorOption.readOnly)) {
223
+ editorInstance.setValue(value);
224
+ } else {
225
+ const model = editorInstance.getModel();
226
+ if (!model) return;
227
+ editorInstance.executeEdits('', [
228
+ {
229
+ range: model.getFullModelRange(),
230
+ text: value,
231
+ forceMoveMarkers: true,
232
+ },
233
+ ]);
234
+ editorInstance.pushUndoStop();
235
+ }
236
+ } finally {
237
+ preventChangeRef.current = false;
238
+ }
239
+ }, [isEditorReady, value, path]);
240
+
241
+ useEffect(() => {
242
+ const model = editorRef.current?.getModel();
243
+ if (isEditorReady && model && language && monaco) {
244
+ monaco.editor.setModelLanguage(model, language);
245
+ }
246
+ }, [isEditorReady, language, path]);
247
+
248
+ useEffect(() => {
249
+ if (isEditorReady && line !== undefined) {
250
+ editorRef.current?.revealLine(line);
251
+ }
252
+ }, [line]);
253
+
254
+ useEffect(() => {
255
+ if (isEditorReady && monaco) monaco.editor.setTheme(theme);
256
+ }, [theme]);
257
+
258
+ <Container
259
+ width={width}
260
+ height={height}
261
+ isEditorReady={isEditorReady}
262
+ loading={loading}
263
+ containerRef={containerRef}
264
+ className={className}
265
+ wrapperProps={wrapperProps}
266
+ />
267
+ }
268
+
269
+ export const Editor = memo(EditorComponent);
270
+ export default Editor;
@@ -0,0 +1,6 @@
1
+ import type { Octane } from 'octane/jsx-runtime';
2
+
3
+ import type { EditorProps } from './types';
4
+
5
+ export declare const Editor: (props: EditorProps) => Octane.JSX.Element;
6
+ export default Editor;
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ export { default, Editor } from './Editor.tsrx';
2
+ export { DiffEditor } from './DiffEditor.tsrx';
3
+ export { useMonaco } from './useMonaco.tsrx';
4
+ export { default as loader } from '@monaco-editor/loader';
5
+ export type {
6
+ BeforeMount,
7
+ DiffBeforeMount,
8
+ DiffEditorProps,
9
+ DiffOnMount,
10
+ EditorProps,
11
+ Monaco,
12
+ MonacoDiffEditor,
13
+ MonacoEditor,
14
+ OnChange,
15
+ OnMount,
16
+ OnValidate,
17
+ Theme,
18
+ WrapperProps,
19
+ } from './types';
package/src/loader.ts ADDED
@@ -0,0 +1,5 @@
1
+ export function isLoaderCancelation(error: unknown): boolean {
2
+ return (
3
+ typeof error === 'object' && error !== null && 'type' in error && error.type === 'cancelation'
4
+ );
5
+ }
package/src/model.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { editor } from 'monaco-editor';
2
+
3
+ import type { Monaco } from './types';
4
+
5
+ const bindingOwnedModels = new WeakSet<editor.ITextModel>();
6
+ const modelLeaseCounts = new WeakMap<editor.ITextModel, number>();
7
+
8
+ export function getOrCreateModel(
9
+ monaco: Monaco,
10
+ value: string,
11
+ language: string,
12
+ path: string | undefined,
13
+ leasedModels: Set<editor.ITextModel>,
14
+ ): editor.ITextModel {
15
+ const uri = path ? monaco.Uri.parse(path) : undefined;
16
+ const existing = uri ? monaco.editor.getModel(uri) : null;
17
+ const model = existing ?? monaco.editor.createModel(value, language || undefined, uri);
18
+ if (!existing) bindingOwnedModels.add(model);
19
+ if (!leasedModels.has(model)) {
20
+ leasedModels.add(model);
21
+ modelLeaseCounts.set(model, (modelLeaseCounts.get(model) ?? 0) + 1);
22
+ }
23
+ return model;
24
+ }
25
+
26
+ export function releaseModels(
27
+ leasedModels: Set<editor.ITextModel>,
28
+ preservedModels: ReadonlySet<editor.ITextModel>,
29
+ ): void {
30
+ for (const model of leasedModels) {
31
+ const nextLeaseCount = Math.max(0, (modelLeaseCounts.get(model) ?? 1) - 1);
32
+ if (nextLeaseCount === 0) {
33
+ modelLeaseCounts.delete(model);
34
+ } else {
35
+ modelLeaseCounts.set(model, nextLeaseCount);
36
+ }
37
+
38
+ if (nextLeaseCount === 0) {
39
+ if (preservedModels.has(model)) {
40
+ // The final keepCurrent* lease transfers ownership out of the binding.
41
+ bindingOwnedModels.delete(model);
42
+ } else if (bindingOwnedModels.has(model)) {
43
+ if (!model.isDisposed()) model.dispose();
44
+ bindingOwnedModels.delete(model);
45
+ }
46
+ }
47
+ }
48
+ leasedModels.clear();
49
+ }
package/src/types.ts ADDED
@@ -0,0 +1,65 @@
1
+ import type { OctaneNode } from 'octane';
2
+ import type { Octane } from 'octane/jsx-runtime';
3
+ import type { editor } from 'monaco-editor';
4
+ import type * as monaco from 'monaco-editor';
5
+
6
+ export type Monaco = typeof monaco;
7
+ export type Theme = 'vs-dark' | 'light';
8
+
9
+ export type MonacoEditor = editor.IStandaloneCodeEditor;
10
+ export type MonacoDiffEditor = editor.IStandaloneDiffEditor;
11
+
12
+ export type BeforeMount = (monaco: Monaco) => void;
13
+ export type OnMount = (editor: MonacoEditor, monaco: Monaco) => void;
14
+ export type OnChange = (value: string | undefined, event: editor.IModelContentChangedEvent) => void;
15
+ export type OnValidate = (markers: editor.IMarker[]) => void;
16
+
17
+ export type DiffBeforeMount = (monaco: Monaco) => void;
18
+ export type DiffOnMount = (editor: MonacoDiffEditor, monaco: Monaco) => void;
19
+
20
+ export type WrapperProps = Omit<Octane.JSX.IntrinsicElements['section'], 'children'>;
21
+
22
+ export interface EditorProps {
23
+ defaultValue?: string;
24
+ defaultLanguage?: string;
25
+ defaultPath?: string;
26
+ value?: string;
27
+ language?: string;
28
+ path?: string;
29
+ theme?: Theme | string;
30
+ line?: number;
31
+ loading?: OctaneNode;
32
+ options?: editor.IStandaloneEditorConstructionOptions;
33
+ overrideServices?: editor.IEditorOverrideServices;
34
+ saveViewState?: boolean;
35
+ keepCurrentModel?: boolean;
36
+ width?: number | string;
37
+ height?: number | string;
38
+ className?: string;
39
+ wrapperProps?: WrapperProps;
40
+ beforeMount?: BeforeMount;
41
+ onMount?: OnMount;
42
+ onChange?: OnChange;
43
+ onValidate?: OnValidate;
44
+ }
45
+
46
+ export interface DiffEditorProps {
47
+ original?: string;
48
+ modified?: string;
49
+ language?: string;
50
+ originalLanguage?: string;
51
+ modifiedLanguage?: string;
52
+ originalModelPath?: string;
53
+ modifiedModelPath?: string;
54
+ keepCurrentOriginalModel?: boolean;
55
+ keepCurrentModifiedModel?: boolean;
56
+ theme?: Theme | string;
57
+ loading?: OctaneNode;
58
+ options?: editor.IDiffEditorConstructionOptions;
59
+ width?: number | string;
60
+ height?: number | string;
61
+ className?: string;
62
+ wrapperProps?: WrapperProps;
63
+ beforeMount?: DiffBeforeMount;
64
+ onMount?: DiffOnMount;
65
+ }
@@ -0,0 +1,30 @@
1
+ import loader from '@monaco-editor/loader';
2
+ import { useEffect, useState } from 'octane';
3
+
4
+ import { isLoaderCancelation } from './loader';
5
+ import type { Monaco } from './types';
6
+
7
+ export function useMonaco(): Monaco | null {
8
+ const [monaco, setMonaco] = useState<Monaco | null>(loader.__getMonacoInstance());
9
+
10
+ useEffect(() => {
11
+ if (monaco) return;
12
+
13
+ let active = true;
14
+ const request = loader.init();
15
+ request.then((loadedMonaco) => {
16
+ if (active) setMonaco(loadedMonaco);
17
+ }).catch((error: unknown) => {
18
+ if (active && !isLoaderCancelation(error)) {
19
+ console.error('Monaco initialization: error:', error);
20
+ }
21
+ });
22
+
23
+ return () => {
24
+ active = false;
25
+ request.cancel();
26
+ };
27
+ }, [monaco]);
28
+
29
+ return monaco;
30
+ }
@@ -0,0 +1,3 @@
1
+ import type { Monaco } from './types';
2
+
3
+ export declare function useMonaco(): Monaco | null;