'use client';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import loader from '@monaco-editor/loader';
import useMount from '../hooks/useMount';
import useUpdate from '../hooks/useUpdate';
import usePrevious from '../hooks/usePrevious';
import { type IDisposable, type editor } from 'monaco-editor';
import { noop, getOrCreateModel } from '../utils';
import { type EditorProps } from './types';
import { type Monaco } from '..';
import MonacoContainer from '../MonacoContainer';
const viewStates = new Map();
function Editor({
defaultValue,
defaultLanguage,
defaultPath,
value,
language,
path,
/* === */
theme = 'light',
line,
loading = 'Loading...',
options = {},
overrideServices = {},
saveViewState = true,
keepCurrentModel = false,
/* === */
width = '100%',
height = '100%',
className,
wrapperProps = {},
/* === */
beforeMount = noop,
onMount = noop,
onChange,
onValidate = noop,
}: EditorProps) {
const [isEditorReady, setIsEditorReady] = useState(false);
const [isMonacoMounting, setIsMonacoMounting] = useState(true);
const monacoRef = useRef<Monaco | null>(null);
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const onMountRef = useRef(onMount);
const beforeMountRef = useRef(beforeMount);
const subscriptionRef = useRef<IDisposable | undefined>(undefined);
const valueRef = useRef(value);
const previousPath = usePrevious(path);
const preventCreation = useRef(false);
const preventTriggerChangeEvent = useRef<boolean>(false);
useMount(() => {
const cancelable = loader.init();
cancelable
.then((monaco) => (monacoRef.current = monaco) && setIsMonacoMounting(false))
.catch(
(error) =>
error?.type !== 'cancelation' && console.error('Monaco initialization: error:', error),
);
return () => (editorRef.current ? disposeEditor() : cancelable.cancel());
});
useUpdate(
() => {
const model = getOrCreateModel(
monacoRef.current!,
defaultValue || value || '',
defaultLanguage || language || '',
path || defaultPath || '',
);
if (model !== editorRef.current?.getModel()) {
if (saveViewState) viewStates.set(previousPath, editorRef.current?.saveViewState());
editorRef.current?.setModel(model);
if (saveViewState) editorRef.current?.restoreViewState(viewStates.get(path));
}
},
[path],
isEditorReady,
);
useUpdate(
() => {
editorRef.current?.updateOptions(options);
},
[options],
isEditorReady,
);
useUpdate(
() => {
if (!editorRef.current || value === undefined) return;
if (editorRef.current.getOption(monacoRef.current!.editor.EditorOption.readOnly)) {
editorRef.current.setValue(value);
} else if (value !== editorRef.current.getValue()) {
preventTriggerChangeEvent.current = true;
editorRef.current.executeEdits('', [
{
range: editorRef.current.getModel()!.getFullModelRange(),
text: value,
forceMoveMarkers: true,
},
]);
editorRef.current.pushUndoStop();
preventTriggerChangeEvent.current = false;
}
},
[value],
isEditorReady,
);
useUpdate(
() => {
const model = editorRef.current?.getModel();
if (isEditorReady && model && language) {
monacoRef.current?.editor.setModelLanguage(model, language);
}
},
[language, isEditorReady],
isEditorReady,
);
useUpdate(
() => {
if (line !== undefined) {
editorRef.current?.revealLine(line);
}
},
[line],
isEditorReady,
);
useUpdate(
() => {
monacoRef.current?.editor.setTheme(theme);
},
[theme],
isEditorReady,
);
const createEditor = useCallback(() => {
if (!containerRef.current || !monacoRef.current) return;
if (!preventCreation.current) {
beforeMountRef.current(monacoRef.current);
const autoCreatedModelPath = path || defaultPath;
const defaultModel = getOrCreateModel(
monacoRef.current,
value || defaultValue || '',
defaultLanguage || language || '',
autoCreatedModelPath || '',
);
editorRef.current = monacoRef.current?.editor.create(
containerRef.current,
{
model: defaultModel,
automaticLayout: true,
...options,
},
overrideServices,
);
saveViewState && editorRef.current.restoreViewState(viewStates.get(autoCreatedModelPath));
monacoRef.current.editor.setTheme(theme);
if (line !== undefined) {
editorRef.current.revealLine(line);
}
setIsEditorReady(true);
preventCreation.current = true;
}
}, [
defaultValue,
defaultLanguage,
defaultPath,
value,
language,
path,
options,
overrideServices,
saveViewState,
theme,
line,
]);
useEffect(() => {
if (isEditorReady) {
onMountRef.current(editorRef.current!, monacoRef.current!);
}
}, [isEditorReady]);
useEffect(() => {
!isMonacoMounting && !isEditorReady && createEditor();
}, [isMonacoMounting, isEditorReady, createEditor]);
valueRef.current = value;
useEffect(() => {
if (isEditorReady && onChange) {
subscriptionRef.current?.dispose();
subscriptionRef.current = editorRef.current?.onDidChangeModelContent((event) => {
if (!preventTriggerChangeEvent.current) {
onChange(editorRef.current!.getValue(), event);
}
});
}
}, [isEditorReady, onChange]);
useEffect(() => {
if (isEditorReady) {
const changeMarkersListener = monacoRef.current!.editor.onDidChangeMarkers((uris) => {
const editorUri = editorRef.current!.getModel()?.uri;
if (editorUri) {
const currentEditorHasMarkerChanges = uris.find((uri) => uri.path === editorUri.path);
if (currentEditorHasMarkerChanges) {
const markers = monacoRef.current!.editor.getModelMarkers({
resource: editorUri,
});
onValidate?.(markers);
}
}
});
return () => {
changeMarkersListener?.dispose();
};
}
return () => {
};
}, [isEditorReady, onValidate]);
function disposeEditor() {
subscriptionRef.current?.dispose();
if (keepCurrentModel) {
saveViewState && viewStates.set(path, editorRef.current!.saveViewState());
} else {
editorRef.current!.getModel()?.dispose();
}
editorRef.current!.dispose();
editorRef.current = null;
preventCreation.current = false;
setIsEditorReady(false);
}
return (
<MonacoContainer
width={width}
height={height}
isEditorReady={isEditorReady}
loading={loading}
_ref={containerRef}
className={className}
wrapperProps={wrapperProps}
/>
);
}
export default Editor;