UNPKG

@typefox/monaco-editor-react

Version:
361 lines 17.1 kB
/* -------------------------------------------------------------------------------------------- * 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