monaco-editor-wrapper
Version:
Wrapper for monaco-vscode-editor-api and monaco-languageclient
339 lines • 13.9 kB
JavaScript
/* --------------------------------------------------------------------------------------------
* 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