UNPKG

monaco-editor-wrapper

Version:
339 lines 13.9 kB
/* -------------------------------------------------------------------------------------------- * Copyright (c) 2024 TypeFox and others. * Licensed under the MIT License. See LICENSE in the package root for license information. * ------------------------------------------------------------------------------------------ */ import * as monaco from '@codingame/monaco-vscode-editor-api'; import { DisposableStore } from '@codingame/monaco-vscode-api/monaco'; import { LogLevel } from '@codingame/monaco-vscode-api'; import { registerExtension, ExtensionHostKind, getExtensionManifests } from '@codingame/monaco-vscode-api/extensions'; import { MonacoLanguageClient } from 'monaco-languageclient'; import { initServices } from 'monaco-languageclient/vscode/services'; import { ConsoleLogger } from 'monaco-languageclient/tools'; import { augmentVscodeApiConfig, checkServiceConsistency } from './vscode/services.js'; import { EditorApp, verifyUrlOrCreateDataUrl } from './editorApp.js'; import { LanguageClientWrapper } from './languageClientWrapper.js'; /** * This class is responsible for the overall ochestration. * It inits, start and disposes the editor apps and the language client (if configured) and provides * access to all required components. */ export class MonacoEditorLanguageClientWrapper { id; editorApp; extensionRegisterResults = new Map(); disposableStore = new DisposableStore(); languageClientWrappers = new Map(); wrapperConfig; logger = new ConsoleLogger(); initAwait; initResolve; startingAwait; startingResolve; disposingAwait; disposingResolve; /** * Perform an isolated initialization of the user services and the languageclient wrapper (if used). */ async init(wrapperConfig) { if (this.isInitializing()) { await this.getInitializingAwait(); } const editorAppConfig = wrapperConfig.editorAppConfig; if ((editorAppConfig?.useDiffEditor ?? false) && !editorAppConfig?.codeResources?.original) { throw new Error(`Use diff editor was used without a valid config. code: ${editorAppConfig?.codeResources?.modified} codeOriginal: ${editorAppConfig?.codeResources?.original}`); } const viewServiceType = wrapperConfig.vscodeApiConfig?.viewsConfig?.viewServiceType ?? 'EditorService'; if (wrapperConfig.$type === 'classic' && (viewServiceType === 'ViewsService' || viewServiceType === 'WorkspaceService')) { throw new Error(`View Service Type "${viewServiceType}" cannot be used with classic configuration.`); } // automatically dispose before re-init, allow to disable this behavior if (wrapperConfig.automaticallyDispose ?? true) { await this.dispose(); } else { // This will throw an error if not disposed before if (this.wrapperConfig !== undefined) { throw new Error('You configured the wrapper to not automatically dispose on init, but did not dispose manually. Please call dispose first if you want to re-start.'); } } try { this.markInitializing(); this.id = wrapperConfig.id ?? Math.floor(Math.random() * 1000001).toString(); this.logger.setLevel(wrapperConfig.logLevel ?? LogLevel.Off); if (!(wrapperConfig.vscodeApiConfig?.vscodeApiInitPerformExternally === true)) { wrapperConfig.vscodeApiConfig = await augmentVscodeApiConfig(wrapperConfig.$type, { vscodeApiConfig: wrapperConfig.vscodeApiConfig ?? {}, logLevel: this.logger.getLevel(), // workaround for classic monaco-editor not applying semanticHighlighting semanticHighlighting: wrapperConfig.editorAppConfig?.editorOptions?.['semanticHighlighting.enabled'] === true }); await initServices(wrapperConfig.vscodeApiConfig, { monacoWorkerFactory: wrapperConfig.editorAppConfig?.monacoWorkerFactory, htmlContainer: wrapperConfig.htmlContainer, caller: `monaco-editor (${this.id})`, performServiceConsistencyChecks: checkServiceConsistency, logger: this.logger }); } this.wrapperConfig = wrapperConfig; if (this.wrapperConfig.languageClientConfigs?.automaticallyInit ?? true) { this.initLanguageClients(); } await this.initExtensions(); this.editorApp = new EditorApp(this.wrapperConfig.$type, this.id, this.wrapperConfig.editorAppConfig, this.logger); await this.editorApp.init(); // eslint-disable-next-line no-useless-catch } catch (e) { throw e; } finally { // in case of rejection, mark as initialized, otherwise the promise will never resolve this.markInitialized(); } } initLanguageClients() { const lccEntries = Object.entries(this.wrapperConfig?.languageClientConfigs?.configs ?? {}); if (lccEntries.length > 0) { for (const [languageId, lcc] of lccEntries) { const lcw = new LanguageClientWrapper({ languageClientConfig: lcc, logger: this.logger }); this.languageClientWrappers.set(languageId, lcw); } } } async initExtensions() { const vscodeApiConfig = this.wrapperConfig?.vscodeApiConfig; if (this.wrapperConfig?.$type === 'extended' && (vscodeApiConfig?.loadThemes === undefined ? true : vscodeApiConfig.loadThemes === true)) { await import('@codingame/monaco-vscode-theme-defaults-default-extension'); } const extensions = this.wrapperConfig?.extensions; if (this.wrapperConfig?.extensions) { const allPromises = []; const extensionIds = []; getExtensionManifests().forEach((ext) => { extensionIds.push(ext.identifier.id); }); for (const extensionConfig of extensions ?? []) { if (!extensionIds.includes(`${extensionConfig.config.publisher}.${extensionConfig.config.name}`)) { const manifest = extensionConfig.config; const extRegResult = registerExtension(manifest, ExtensionHostKind.LocalProcess); this.extensionRegisterResults.set(manifest.name, extRegResult); if (extensionConfig.filesOrContents && Object.hasOwn(extRegResult, 'registerFileUrl')) { for (const entry of extensionConfig.filesOrContents) { this.disposableStore.add(extRegResult.registerFileUrl(entry[0], verifyUrlOrCreateDataUrl(entry[1]))); } } allPromises.push(extRegResult.whenReady()); } } await Promise.all(allPromises); } } ; markInitializing() { this.initAwait = new Promise((resolve) => { this.initResolve = resolve; }); } markInitialized() { this.initResolve(); this.initAwait = undefined; } isInitializing() { return this.initAwait !== undefined; } getInitializingAwait() { return this.initAwait; } getWrapperConfig() { return this.wrapperConfig; } getExtensionRegisterResult(extensionName) { return this.extensionRegisterResults.get(extensionName); } /** * Performs a full user configuration and the languageclient wrapper (if used) init and then start the application. */ async initAndStart(wrapperConfig) { await this.init(wrapperConfig); await this.start(); } /** * Does not perform any user configuration or other application init and just starts the application. */ async start(htmlContainer) { if (this.isStarting()) { await this.getStartingAwait(); } if (this.wrapperConfig === undefined) { throw new Error('No init was performed. Please call init() before start()'); } this.markStarting(); try { const viewServiceType = this.wrapperConfig.vscodeApiConfig?.viewsConfig?.viewServiceType; if (viewServiceType === 'EditorService' || viewServiceType === undefined) { this.logger.info(`Starting monaco-editor (${this.id})`); const html = htmlContainer === undefined ? this.wrapperConfig.htmlContainer : htmlContainer; if (html === undefined) { throw new Error('No html container provided. Unable to start monaco-editor.'); } else { await this.editorApp?.createEditors(html); } } else { this.logger.info('No EditorService configured. monaco-editor will not be started.'); } if (this.wrapperConfig.languageClientConfigs?.automaticallyStart ?? true) { await this.startLanguageClients(); } // eslint-disable-next-line no-useless-catch } catch (e) { throw e; } finally { // in case of rejection, mark as started, otherwise the promise will never resolve this.markStarted(); } } async startLanguageClients() { const allPromises = []; for (const lcw of this.languageClientWrappers.values()) { allPromises.push(lcw.start()); } return Promise.all(allPromises); } markStarting() { this.startingAwait = new Promise((resolve) => { this.startingResolve = resolve; }); } markStarted() { this.startingResolve(); this.startingAwait = undefined; } isStarting() { return this.startingAwait !== undefined; } getStartingAwait() { return this.startingAwait; } isStarted() { // fast-fail if (!(this.editorApp?.haveEditor() ?? false)) { return false; } for (const lcw of this.languageClientWrappers.values()) { if (lcw.haveLanguageClient()) { // as soon as one is not started return if (!lcw.isStarted()) { return false; } } } return true; } haveLanguageClients() { return this.languageClientWrappers.size > 0; } getEditorApp() { return this.editorApp; } getEditor() { return this.editorApp?.getEditor(); } getDiffEditor() { return this.editorApp?.getDiffEditor(); } getLanguageClientWrapper(languageId) { return this.languageClientWrappers.get(languageId); } getLanguageClient(languageId) { return this.languageClientWrappers.get(languageId)?.getLanguageClient(); } getTextModels() { return this.editorApp?.getTextModels(); } getWorker(languageId) { return this.languageClientWrappers.get(languageId)?.getWorker(); } getLogger() { return this.logger; } async updateCodeResources(codeResources) { return this.editorApp?.updateCodeResources(codeResources); } registerTextChangedCallback(onTextChanged) { this.editorApp?.registerOnTextChangedCallbacks(onTextChanged); } reportStatus() { const status = []; status.push('Wrapper status:'); status.push(`Editor: ${this.editorApp?.getEditor()?.getId()}`); status.push(`DiffEditor: ${this.editorApp?.getDiffEditor()?.getId()}`); return status; } /** * Disposes all application and editor resources, plus the languageclient (if used). */ async dispose() { if (this.isDisposing()) { await this.getDisposingAwait(); } this.markDisposing(); await this.editorApp?.dispose(); this.editorApp = undefined; this.extensionRegisterResults.forEach((k) => k.dispose()); this.disposableStore.dispose(); // re-create disposable stores this.disposableStore = new DisposableStore(); try { if (this.wrapperConfig?.languageClientConfigs?.automaticallyDispose ?? true) { await this.disposeLanguageClients(); } // eslint-disable-next-line no-useless-catch } catch (e) { throw e; } finally { // in case of rejection, mark as stopped, otherwise the promise will never resolve this.languageClientWrappers.clear(); this.wrapperConfig = undefined; this.markDisposed(); } } async disposeLanguageClients() { const disposeWorker = this.wrapperConfig?.languageClientConfigs?.automaticallyDisposeWorkers ?? false; const allPromises = []; for (const lcw of this.languageClientWrappers.values()) { if (lcw.haveLanguageClient()) { allPromises.push(lcw.disposeLanguageClient(disposeWorker)); } } return Promise.all(allPromises); } markDisposing() { this.disposingAwait = new Promise((resolve) => { this.disposingResolve = resolve; }); } markDisposed() { this.disposingResolve(); this.disposingAwait = undefined; } isDisposing() { return this.disposingAwait !== undefined; } getDisposingAwait() { return this.disposingAwait; } updateLayout() { this.editorApp?.updateLayout(); } } //# sourceMappingURL=wrapper.js.map