@typefox/monaco-editor-react
Version:
React component for Monaco-Editor and Monaco Languageclient
361 lines • 17.1 kB
JavaScript
/* --------------------------------------------------------------------------------------------
* Copyright (c) 2024 TypeFox and others.
* Licensed under the MIT License. See LICENSE in the package root for license information.
* ------------------------------------------------------------------------------------------ */
import { LogLevel } from '@codingame/monaco-vscode-api';
import { ConsoleLogger } from '@codingame/monaco-vscode-log-service-override';
import { EditorApp } from 'monaco-languageclient/editorApp';
import { LanguageClientManager } from 'monaco-languageclient/lcwrapper';
import { getEnhancedMonacoEnvironment, MonacoVscodeApiWrapper } from 'monaco-languageclient/vscodeApiWrapper';
import React, { useEffect, useRef } from 'react';
// All must be outside of the component as they ars valid across all instances and should not be re-created
let apiWrapper;
const lcsManager = new LanguageClientManager();
const haveEditorService = () => {
return getEnhancedMonacoEnvironment().viewServiceType === 'EditorService';
};
const logger = new ConsoleLogger(LogLevel.Off);
const runQueue = [];
let runQueueLock = true;
let intervalId = undefined;
const queueIntervalMs = 10;
const addQueue = (queueEntry) => {
debugLogging('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
debugLogging(`Adding to queue: ${queueEntry.id}: QUEUE SIZE before: ${runQueue.length}`);
runQueue.push(queueEntry);
kickQueue();
};
const executeQueue = async () => {
if (runQueue.length > 0) {
runQueueLock = true;
while (runQueue.length > 0) {
const lengthBefore = runQueue.length;
const queueObj = runQueue.shift();
debugLogging('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<');
debugLogging(`QUEUE ${queueObj?.id} start: SIZE before: ${lengthBefore}`);
await queueObj?.func(queueObj.currentContainer);
debugLogging(`QUEUE ${queueObj?.id} end: SIZE after: ${runQueue.length}`);
}
runQueueLock = false;
}
};
const kickQueue = () => {
if (intervalId === undefined && runQueue.length > 0) {
intervalId = setInterval(async () => {
debugLogging(`Checking queue (lock state: ${runQueueLock})`);
if (!runQueueLock) {
await executeQueue();
stopQueue();
}
}, queueIntervalMs);
}
};
const stopQueue = () => {
if (intervalId !== undefined && runQueue.length === 0) {
debugLogging('Stopping queue...');
clearInterval(intervalId);
intervalId = undefined;
}
};
const debugLogging = (id) => {
const now = new Date(Date.now());
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
const milliseconds = now.getMilliseconds().toString().padStart(3, '0');
logger.debug(`[${hours}:${minutes}:${seconds}.${milliseconds}]: ${id}`);
};
export const MonacoEditorReactComp = (props) => {
const { style, className, vscodeApiConfig, editorAppConfig, languageClientConfig, onVscodeApiInitDone, onEditorStartDone, onLanguageClientsStartDone, onTextChanged, onConfigProcessed, onError, onDisposeEditor, onDisposeLanguageClient, logLevel, triggerReprocessConfig, enforceLanguageClientDispose } = props;
const editorAppRef = useRef(undefined);
const containerRef = useRef(null);
const modifiedCodeUriRef = useRef(undefined);
const modifiedCodeRef = useRef(undefined);
const originalCodeUriRef = useRef(undefined);
const originalCodeRef = useRef(undefined);
const onTextChangedRef = useRef(onTextChanged);
const launchingRef = useRef(false);
const editorAppConfigRef = useRef(undefined);
const triggerReprocessConfigRef = useRef(0);
const enforceLanguageClientDisposeRef = useRef(undefined);
const performErrorHandling = (error) => {
debugLogging(`ERROR: ${error.message}`);
if (onError) {
onError(error);
}
else {
debugLogging(`INTERCEPTED Error: ${error}. Stopping queue...`);
runQueueLock = false;
throw error;
}
};
const performGlobalInit = async () => {
if (containerRef.current === null) {
performErrorHandling(new Error('No htmlContainer found! Aborting...'));
}
const envEnhanced = getEnhancedMonacoEnvironment();
if (vscodeApiConfig === undefined && envEnhanced.vscodeApiInitialised !== true) {
throw new Error('vscodeApiConfig is not provided, but the monaco-vscode-api is not initialized! Aborting...');
}
// init will only performed once
if (envEnhanced.vscodeApiInitialising !== true) {
apiWrapper = new MonacoVscodeApiWrapper(vscodeApiConfig);
const globalInitFunc = async () => {
try {
if (apiWrapper === undefined)
throw new Error('Unexpected error occurred: apiWrapper is not available! Aborting...');
await apiWrapper.start();
onVscodeApiInitDone?.(apiWrapper);
runQueueLock = false;
}
catch (error) {
performErrorHandling(error);
}
};
globalInitFunc();
}
else if (envEnhanced.vscodeApiInitialised === true) {
if (runQueueLock && intervalId !== undefined) {
runQueueLock = false;
}
}
};
const editorInit = async (htmlContainer) => {
try {
// it is possible to run without an editorApp, when the ViewsService or WorkbenchService
if (haveEditorService()) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (htmlContainer === null || (htmlContainer !== null && htmlContainer.parentElement === null)) {
debugLogging('INIT EDITOR: Unable to create editor. HTML container or the parent is missing.');
}
else {
if (editorAppRef.current === undefined && !launchingRef.current) {
launchingRef.current = true;
editorAppRef.current = new EditorApp(editorAppConfigRef.current);
if (editorAppRef.current.isStarting() === true || editorAppRef.current.isDisposing() === true) {
await Promise.all([
editorAppRef.current.getStartingAwait(),
editorAppRef.current.getDisposingAwait()
]);
}
updateModelRelatedRefs();
editorAppRef.current.registerOnTextChangedCallback((textChanges) => {
if (textChanges.modified !== undefined) {
modifiedCodeRef.current = textChanges.modified;
}
if (textChanges.original !== undefined) {
originalCodeRef.current = textChanges.original;
}
if (onTextChangedRef.current !== undefined) {
onTextChangedRef.current(textChanges);
}
});
// await retrieveContainerRef('INIT EDITOR');
await editorAppRef.current.start(htmlContainer);
onEditorStartDone?.(editorAppRef.current);
launchingRef.current = false;
debugLogging('INIT EDITOR: Editor start was successful.');
}
else {
debugLogging('INIT EDITOR: Editor was already started.');
}
}
}
else {
debugLogging('INIT EDITOR: Do nothing: Using ViewsService');
}
}
catch (error) {
performErrorHandling(error);
}
};
const updateEditorModel = async () => {
try {
if (!launchingRef.current && editorAppRef.current) {
editorAppRef.current.updateCodeResources(editorAppConfigRef.current?.codeResources);
updateModelRelatedRefs();
onConfigProcessed?.({ modelUpdated: true, textUpdated: true, editorApp: editorAppRef.current });
debugLogging('UPDATE EDITOR MODEL: Model was updated.');
}
else {
onConfigProcessed?.({ modelUpdated: false, textUpdated: false, editorApp: editorAppRef.current });
debugLogging('UPDATE EDITOR MODEL: No editor is avilable. Model update was not possible.');
}
}
catch (error) {
performErrorHandling(error);
}
};
const updateModelRelatedRefs = () => {
modifiedCodeRef.current = editorAppConfigRef.current?.codeResources?.modified?.text;
originalCodeRef.current = editorAppConfigRef.current?.codeResources?.original?.text;
modifiedCodeUriRef.current = editorAppConfigRef.current?.codeResources?.modified?.uri;
originalCodeUriRef.current = editorAppConfigRef.current?.codeResources?.original?.uri;
};
const disposeEditor = async () => {
try {
// dispose editor if used
if (editorAppRef.current !== undefined) {
await editorAppRef.current.dispose();
editorAppRef.current = undefined;
onDisposeEditor?.();
debugLogging('DISPOSE: EditorApp was disposed');
}
else {
debugLogging('DISPOSE: EditorApp is not disposed');
}
}
catch (error) {
performErrorHandling(error);
}
};
const processConfig = () => {
let modelUpdated = false;
let textUpdated = false;
try {
debugLogging('CONFIG PROCESSED: Started');
if (!launchingRef.current && editorAppRef.current) {
if (editorAppConfigRef.current?.codeResources !== undefined) {
const newModifiedCodeUri = editorAppConfigRef.current.codeResources.modified?.uri;
const newOriginalCodeUri = editorAppConfigRef.current.codeResources.original?.uri;
const modifiedUri = modifiedCodeUriRef.current !== newModifiedCodeUri ? newModifiedCodeUri : undefined;
const originalUri = originalCodeUriRef.current !== newOriginalCodeUri ? newOriginalCodeUri : undefined;
// re-create the editor if the URIs have changed
if (modifiedUri !== undefined || originalUri !== undefined) {
modelUpdated = true;
}
else {
const newModifiedCode = editorAppConfigRef.current.codeResources.modified?.text;
const newOriginalCode = editorAppConfigRef.current.codeResources.original?.text;
const modified = modifiedCodeRef.current !== newModifiedCode ? newModifiedCode : undefined;
const original = originalCodeRef.current !== newOriginalCode ? newOriginalCode : undefined;
if (modified !== undefined || original !== undefined) {
textUpdated = true;
editorAppRef.current.updateCode({ modified, original });
}
}
}
if (editorAppConfigRef.current?.editorOptions !== undefined) {
if (!editorAppRef.current.isDiffEditor()) {
editorAppRef.current.getEditor()?.updateOptions(editorAppConfigRef.current.editorOptions);
}
}
if (editorAppConfigRef.current?.diffEditorOptions !== undefined) {
if (editorAppRef.current.isDiffEditor()) {
editorAppRef.current.getDiffEditor()?.updateOptions(editorAppConfigRef.current.diffEditorOptions);
}
}
}
// notitfy now if no async model update was necessary
if (!modelUpdated) {
onConfigProcessed?.({ modelUpdated, textUpdated, editorApp: editorAppRef.current });
}
debugLogging('CONFIG PROCESSED: Done');
}
catch (error) {
performErrorHandling(error);
}
return modelUpdated;
};
useEffect(() => {
// fast-fail
if (editorAppConfig === undefined)
return;
// always try to perform global init. Reason: we cannot ensure order
performGlobalInit();
editorAppConfigRef.current = editorAppConfig;
// it is possible to run without an editorApp, when the ViewsService or WorkbenchService
if (haveEditorService()) {
if (editorAppRef.current === undefined) {
addQueue({ id: 'editorInit', func: editorInit, currentContainer: containerRef.current });
}
else {
debugLogging('CHECK EDITOR: Editor already created. No queueing necessary.');
}
}
else {
debugLogging('INIT EDITOR: Do nothing: Using ViewsService');
}
}, [editorAppConfig]);
useEffect(() => {
const triggerValue = triggerReprocessConfig ?? 0;
if (triggerValue > triggerReprocessConfigRef.current) {
debugLogging('REPROCESS CONFIG: Triggered');
const updateModel = processConfig();
if (updateModel) {
addQueue({ id: 'modelUpdate', func: updateEditorModel, currentContainer: containerRef.current });
}
triggerReprocessConfigRef.current = triggerValue;
}
else {
debugLogging('REPROCESS CONFIG: Denied');
}
}, [triggerReprocessConfig]);
const lcInitFunc = async () => {
try {
await lcsManager.start();
onLanguageClientsStartDone?.(lcsManager);
}
catch (error) {
performErrorHandling(error);
}
};
useEffect(() => {
// fast-fail
if (languageClientConfig === undefined)
return;
// always try to perform global init. Reason: we cannot ensure order
performGlobalInit();
lcsManager.setLogLevel(languageClientConfig.logLevel);
lcsManager.setConfig(languageClientConfig);
if (!lcsManager.isStarted() && (enforceLanguageClientDisposeRef.current === undefined || !enforceLanguageClientDisposeRef.current)) {
addQueue({ id: 'lcInit', func: lcInitFunc, currentContainer: containerRef.current });
}
else {
debugLogging('INIT LC: Language client is already running. No need to schedule async start.');
}
}, [languageClientConfig]);
useEffect(() => {
enforceLanguageClientDisposeRef.current = enforceLanguageClientDispose === true;
if (enforceLanguageClientDisposeRef.current === true) {
const disposeLCFunc = async () => {
try {
await lcsManager.dispose();
onDisposeLanguageClient?.();
}
catch (error) {
// The language client may throw an error during disposal, but we want to continue anyway
performErrorHandling(new Error(`Unexpected error occurred during disposal of the language client: ${error}`));
}
};
if (lcsManager.isStarted()) {
addQueue({ id: 'lcDispose', func: disposeLCFunc, currentContainer: containerRef.current });
}
else {
debugLogging('ENFORCE DISPOSE LC: Denied: No language client is running.');
}
}
else {
debugLogging('ENFORCE DISPOSE LC: Denied: enforceLanguageClientDisposeRef.current is false.');
if (!lcsManager.isStarted()) {
addQueue({ id: 'lcInit', func: lcInitFunc, currentContainer: containerRef.current });
}
}
}, [enforceLanguageClientDispose]);
useEffect(() => {
// this part runs on mount (componentDidMount)
// always try to perform global init. Reason: we cannot ensure order
performGlobalInit();
// this part runs on unmount (componentWillUnmount)
return () => {
addQueue({ id: 'disposeEditor', func: disposeEditor, currentContainer: containerRef.current });
};
}, []);
useEffect(() => {
if (logLevel !== undefined) {
logger.setLevel(logLevel);
}
}, [logLevel]);
return (React.createElement("div", { ref: containerRef, style: style, className: className }));
};
//# sourceMappingURL=index.js.map