Skip to content

Commit 1e7d55a

Browse files
authored
Merge pull request #57 from beNative/codex/fix-monaco-text-editor-visibility
Fix Monaco editor initialization regression
2 parents 0934327 + 1f26f54 commit 1e7d55a

3 files changed

Lines changed: 230 additions & 114 deletions

File tree

components/CodeEditor.tsx

Lines changed: 76 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useRef, useEffect, forwardRef, useImperativeHandle, useCallback,
22
import { useTheme } from '../hooks/useTheme';
33
import { MONACO_KEYBINDING_DEFINITIONS } from '../services/editor/monacoKeybindings';
44
import { DEFAULT_SETTINGS } from '../constants';
5+
import { ensureMonaco } from '../services/editor/monacoLoader';
56

67
// Let TypeScript know monaco is available on the window
78
declare const monaco: any;
@@ -114,6 +115,7 @@ const toMonacoKeybinding = (monacoApi: any, keys: string[]): number | null => {
114115
const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, language, onChange, onScroll, customShortcuts = {}, fontFamily, fontSize }, ref) => {
115116
const editorRef = useRef<HTMLDivElement>(null);
116117
const monacoInstanceRef = useRef<any>(null);
118+
const monacoApiRef = useRef<any>(null);
117119
const { theme } = useTheme();
118120
const contentRef = useRef(content);
119121
const customShortcutsRef = useRef<Record<string, string[]>>({});
@@ -135,7 +137,12 @@ const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, lan
135137
monacoInstanceRef.current?.getAction('editor.action.formatDocument')?.run();
136138
},
137139
setScrollTop(scrollTop: number) {
138-
monacoInstanceRef.current?.setScrollTop(scrollTop, monaco.editor.ScrollType.Immediate);
140+
const scrollType = monacoApiRef.current?.editor?.ScrollType?.Immediate;
141+
if (scrollType) {
142+
monacoInstanceRef.current?.setScrollTop(scrollTop, scrollType);
143+
} else {
144+
monacoInstanceRef.current?.setScrollTop(scrollTop);
145+
}
139146
},
140147
getScrollInfo() {
141148
return new Promise(resolve => {
@@ -167,7 +174,8 @@ const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, lan
167174
}, []);
168175

169176
const applyEditorShortcuts = useCallback(() => {
170-
if (!monacoInstanceRef.current || typeof monaco === 'undefined') {
177+
const monacoApi = monacoApiRef.current;
178+
if (!monacoInstanceRef.current || !monacoApi) {
171179
return;
172180
}
173181

@@ -180,7 +188,7 @@ const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, lan
180188
return;
181189
}
182190

183-
const keybinding = toMonacoKeybinding(monaco, keys);
191+
const keybinding = toMonacoKeybinding(monacoApi, keys);
184192
if (keybinding === null) {
185193
return;
186194
}
@@ -211,77 +219,79 @@ const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, lan
211219
}, [content]);
212220

213221
useEffect(() => {
214-
if (editorRef.current && typeof ((window as any).require) !== 'undefined') {
215-
// Configure Monaco Environment to load workers from CDN. This is crucial for syntax highlighting.
216-
if (!(window as any).MonacoEnvironment) {
217-
(window as any).MonacoEnvironment = {
218-
getWorkerUrl: function (_moduleId: any, label: string) {
219-
const CDN_PATH = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs';
220-
if (label === 'json') return `${CDN_PATH}/language/json/json.worker.js`;
221-
if (label === 'css' || label === 'scss' || label === 'less') return `${CDN_PATH}/language/css/css.worker.js`;
222-
if (label === 'html' || label === 'handlebars' || label === 'razor') return `${CDN_PATH}/language/html/html.worker.js`;
223-
if (label === 'typescript' || label === 'javascript') return `${CDN_PATH}/language/typescript/ts.worker.js`;
224-
return `${CDN_PATH}/editor/editor.worker.js`;
225-
},
226-
};
222+
let isCancelled = false;
223+
224+
const initializeEditor = async () => {
225+
if (!editorRef.current) {
226+
return;
227227
}
228228

229-
(window as any).require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs' }});
230-
(window as any).require(['vs/editor/editor.main'], () => {
231-
if (editorRef.current) {
232-
// Ensure any previous instance is disposed
233-
if (monacoInstanceRef.current) {
234-
disposeEditorShortcuts();
235-
monacoInstanceRef.current.dispose();
236-
}
229+
try {
230+
const monacoApi = await ensureMonaco();
231+
if (!monacoApi || isCancelled || !editorRef.current) {
232+
return;
233+
}
237234

238-
const editorInstance = monaco.editor.create(editorRef.current, {
239-
value: content,
240-
language: language || 'plaintext',
241-
theme: theme === 'dark' ? 'vs-dark' : 'vs',
242-
automaticLayout: true,
243-
fontSize: computedFontSize,
244-
fontFamily: computedFontFamily,
245-
minimap: {
246-
enabled: true,
247-
},
248-
wordWrap: 'on',
249-
folding: true,
250-
showFoldingControls: 'always',
251-
bracketPairColorization: {
252-
enabled: true,
253-
},
254-
});
235+
monacoApiRef.current = monacoApi;
255236

256-
editorInstance.onDidChangeModelContent(() => {
257-
const currentValue = editorInstance.getValue();
258-
if (currentValue !== contentRef.current) {
259-
onChange(currentValue);
260-
}
261-
});
237+
if (monacoInstanceRef.current) {
238+
disposeEditorShortcuts();
239+
monacoInstanceRef.current.dispose();
240+
}
262241

263-
editorInstance.onDidScrollChange((e: any) => {
264-
if (e.scrollTopChanged) {
265-
onScroll?.({
266-
scrollTop: e.scrollTop,
267-
scrollHeight: e.scrollHeight,
268-
clientHeight: editorInstance.getLayoutInfo().height
269-
});
270-
}
271-
});
242+
const editorInstance = monacoApi.editor.create(editorRef.current, {
243+
value: content,
244+
language: language || 'plaintext',
245+
theme: theme === 'dark' ? 'vs-dark' : 'vs',
246+
automaticLayout: true,
247+
fontSize: computedFontSize,
248+
fontFamily: computedFontFamily,
249+
minimap: {
250+
enabled: true,
251+
},
252+
wordWrap: 'on',
253+
folding: true,
254+
showFoldingControls: 'always',
255+
bracketPairColorization: {
256+
enabled: true,
257+
},
258+
});
272259

273-
monacoInstanceRef.current = editorInstance;
274-
applyEditorShortcuts();
275-
}
276-
});
277-
}
260+
editorInstance.onDidChangeModelContent(() => {
261+
const currentValue = editorInstance.getValue();
262+
if (currentValue !== contentRef.current) {
263+
onChange(currentValue);
264+
}
265+
});
266+
267+
editorInstance.onDidScrollChange((e: any) => {
268+
if (e.scrollTopChanged) {
269+
onScroll?.({
270+
scrollTop: e.scrollTop,
271+
scrollHeight: e.scrollHeight,
272+
clientHeight: editorInstance.getLayoutInfo().height
273+
});
274+
}
275+
});
276+
277+
monacoInstanceRef.current = editorInstance;
278+
applyEditorShortcuts();
279+
} catch (error) {
280+
// eslint-disable-next-line no-console
281+
console.error('Failed to initialize Monaco editor', error);
282+
}
283+
};
284+
285+
initializeEditor();
278286

279287
return () => {
288+
isCancelled = true;
280289
disposeEditorShortcuts();
281290
if (monacoInstanceRef.current) {
282291
monacoInstanceRef.current.dispose();
283292
monacoInstanceRef.current = null;
284293
}
294+
monacoApiRef.current = null;
285295
};
286296
}, [onChange, onScroll, applyEditorShortcuts, disposeEditorShortcuts, computedFontFamily, computedFontSize]);
287297

@@ -299,8 +309,8 @@ const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, lan
299309

300310
// Effect to update theme
301311
useEffect(() => {
302-
if (monacoInstanceRef.current) {
303-
monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'vs');
312+
if (monacoInstanceRef.current && monacoApiRef.current) {
313+
monacoApiRef.current.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'vs');
304314
}
305315
}, [theme]);
306316

@@ -309,11 +319,11 @@ const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({ content, lan
309319
monacoInstanceRef.current.updateOptions({ fontFamily: computedFontFamily, fontSize: computedFontSize });
310320
}
311321
}, [computedFontFamily, computedFontSize]);
312-
322+
313323
// Effect to update language
314324
useEffect(() => {
315-
if (monacoInstanceRef.current && monacoInstanceRef.current.getModel()) {
316-
monaco.editor.setModelLanguage(monacoInstanceRef.current.getModel(), language || 'plaintext');
325+
if (monacoInstanceRef.current && monacoInstanceRef.current.getModel() && monacoApiRef.current) {
326+
monacoApiRef.current.editor.setModelLanguage(monacoInstanceRef.current.getModel(), language || 'plaintext');
317327
}
318328
}, [language]);
319329

components/MonacoDiffEditor.tsx

Lines changed: 63 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useRef, useEffect, useCallback, useMemo } from 'react';
22
import { useTheme } from '../hooks/useTheme';
33
import { DEFAULT_SETTINGS } from '../constants';
4+
import { ensureMonaco } from '../services/editor/monacoLoader';
45

56
// Let TypeScript know monaco is available on the window
67
declare const monaco: any;
@@ -20,6 +21,7 @@ interface MonacoDiffEditorProps {
2021
const MonacoDiffEditor: React.FC<MonacoDiffEditorProps> = ({ oldText, newText, language, renderMode = 'side-by-side', readOnly = false, onChange, onScroll, fontFamily, fontSize }) => {
2122
const editorRef = useRef<HTMLDivElement>(null);
2223
const editorInstanceRef = useRef<any>(null);
24+
const monacoApiRef = useRef<any>(null);
2325
const { theme } = useTheme();
2426
const modelsRef = useRef<{ original: any; modified: any } | null>(null);
2527
const changeListenerRef = useRef<{ dispose: () => void } | null>(null);
@@ -48,73 +50,85 @@ const MonacoDiffEditor: React.FC<MonacoDiffEditorProps> = ({ oldText, newText, l
4850
}, []);
4951

5052
useEffect(() => {
51-
if (!editorRef.current || typeof ((window as any).require) === 'undefined') {
53+
if (!editorRef.current) {
5254
return;
5355
}
5456

5557
let isCancelled = false;
5658

57-
(window as any).require(['vs/editor/editor.main'], () => {
58-
if (!editorRef.current || isCancelled) return;
59+
const initializeDiffEditor = async () => {
60+
try {
61+
const monacoApi = await ensureMonaco();
62+
if (!monacoApi || isCancelled || !editorRef.current) {
63+
return;
64+
}
65+
66+
monacoApiRef.current = monacoApi;
67+
68+
if (!editorInstanceRef.current) {
69+
editorInstanceRef.current = monacoApi.editor.createDiffEditor(editorRef.current, {
70+
originalEditable: false,
71+
readOnly,
72+
automaticLayout: true,
73+
fontSize: computedFontSize,
74+
fontFamily: computedFontFamily,
75+
wordWrap: 'on',
76+
renderSideBySide: renderMode !== 'inline',
77+
minimap: { enabled: false },
78+
diffWordWrap: 'on',
79+
});
80+
}
5981

60-
if (!editorInstanceRef.current) {
61-
editorInstanceRef.current = monaco.editor.createDiffEditor(editorRef.current, {
62-
originalEditable: false,
82+
const editor = editorInstanceRef.current;
83+
editor.updateOptions({
6384
readOnly,
64-
automaticLayout: true,
65-
fontSize: computedFontSize,
66-
fontFamily: computedFontFamily,
67-
wordWrap: 'on',
6885
renderSideBySide: renderMode !== 'inline',
69-
minimap: { enabled: false },
7086
diffWordWrap: 'on',
87+
fontFamily: computedFontFamily,
88+
fontSize: computedFontSize,
7189
});
72-
}
73-
74-
const editor = editorInstanceRef.current;
75-
editor.updateOptions({
76-
readOnly,
77-
renderSideBySide: renderMode !== 'inline',
78-
diffWordWrap: 'on',
79-
fontFamily: computedFontFamily,
80-
fontSize: computedFontSize,
81-
});
82-
83-
monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'vs');
8490

85-
const originalModel = monaco.editor.createModel(oldText ?? '', language);
86-
const modifiedModel = monaco.editor.createModel(newText ?? '', language);
91+
monacoApi.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'vs');
8792

88-
editor.setModel({
89-
original: originalModel,
90-
modified: modifiedModel,
91-
});
93+
const originalModel = monacoApi.editor.createModel(oldText ?? '', language);
94+
const modifiedModel = monacoApi.editor.createModel(newText ?? '', language);
9295

93-
const previousModels = modelsRef.current;
94-
modelsRef.current = { original: originalModel, modified: modifiedModel };
95-
previousModels?.original?.dispose();
96-
previousModels?.modified?.dispose();
96+
editor.setModel({
97+
original: originalModel,
98+
modified: modifiedModel,
99+
});
97100

98-
disposeListeners();
101+
const previousModels = modelsRef.current;
102+
modelsRef.current = { original: originalModel, modified: modifiedModel };
103+
previousModels?.original?.dispose();
104+
previousModels?.modified?.dispose();
99105

100-
const modifiedEditor = editor.getModifiedEditor();
106+
disposeListeners();
101107

102-
if (onChange && !readOnly) {
103-
changeListenerRef.current = modifiedEditor.onDidChangeModelContent(() => {
104-
onChange(modifiedEditor.getValue());
105-
});
106-
}
108+
const modifiedEditor = editor.getModifiedEditor();
107109

108-
if (onScroll) {
109-
scrollListenerRef.current = modifiedEditor.onDidScrollChange(() => {
110-
onScroll({
111-
scrollTop: modifiedEditor.getScrollTop(),
112-
scrollHeight: modifiedEditor.getScrollHeight(),
113-
clientHeight: modifiedEditor.getLayoutInfo().height,
110+
if (onChange && !readOnly) {
111+
changeListenerRef.current = modifiedEditor.onDidChangeModelContent(() => {
112+
onChange(modifiedEditor.getValue());
114113
});
115-
});
114+
}
115+
116+
if (onScroll) {
117+
scrollListenerRef.current = modifiedEditor.onDidScrollChange(() => {
118+
onScroll({
119+
scrollTop: modifiedEditor.getScrollTop(),
120+
scrollHeight: modifiedEditor.getScrollHeight(),
121+
clientHeight: modifiedEditor.getLayoutInfo().height,
122+
});
123+
});
124+
}
125+
} catch (error) {
126+
// eslint-disable-next-line no-console
127+
console.error('Failed to initialize Monaco diff editor', error);
116128
}
117-
});
129+
};
130+
131+
initializeDiffEditor();
118132

119133
return () => {
120134
isCancelled = true;
@@ -143,6 +157,7 @@ const MonacoDiffEditor: React.FC<MonacoDiffEditorProps> = ({ oldText, newText, l
143157
modelsRef.current.modified?.dispose();
144158
modelsRef.current = null;
145159
}
160+
monacoApiRef.current = null;
146161
};
147162
}, [disposeListeners]);
148163

0 commit comments

Comments
 (0)