monaco-editor-wrapper
Version:
Wrapper for monaco-vscode-editor-api and monaco-languageclient
299 lines • 12.8 kB
JavaScript
/* --------------------------------------------------------------------------------------------
* Copyright (c) 2024 TypeFox and others.
* Licensed under the MIT License. See LICENSE in the package root for license information.
* ------------------------------------------------------------------------------------------ */
import * as vscode from 'vscode';
import * as monaco from '@codingame/monaco-vscode-editor-api';
import { LogLevel } from '@codingame/monaco-vscode-api';
import { createModelReference } from '@codingame/monaco-vscode-api/monaco';
import { ConfigurationTarget, IConfigurationService, StandaloneServices } from '@codingame/monaco-vscode-api';
/**
* This is the base class for both Monaco Ediotor Apps:
* - EditorAppClassic
* - EditorAppExtended
*
* It provides the generic functionality for both implementations.
*/
export class EditorApp {
$type;
id;
config;
logger;
editor;
diffEditor;
modelRefs;
onTextChanged;
textChangedDiposeables = {};
modelDisposables = {};
modelRefDisposeTimeout = -1;
constructor($type, id, userAppConfig, logger) {
this.$type = $type;
this.id = id;
this.logger = logger;
this.config = {
codeResources: userAppConfig?.codeResources ?? undefined,
useDiffEditor: userAppConfig?.useDiffEditor ?? false,
readOnly: userAppConfig?.readOnly ?? false,
domReadOnly: userAppConfig?.domReadOnly ?? false,
overrideAutomaticLayout: userAppConfig?.overrideAutomaticLayout ?? true
};
this.config.editorOptions = {
...userAppConfig?.editorOptions,
automaticLayout: userAppConfig?.overrideAutomaticLayout ?? true
};
this.config.diffEditorOptions = {
...userAppConfig?.diffEditorOptions,
automaticLayout: userAppConfig?.overrideAutomaticLayout ?? true
};
this.config.languageDef = userAppConfig?.languageDef;
}
getConfig() {
return this.config;
}
haveEditor() {
return this.editor !== undefined || this.diffEditor !== undefined;
}
getEditor() {
return this.editor;
}
getDiffEditor() {
return this.diffEditor;
}
getTextModels() {
return {
modified: this.modelRefs.modified.object.textEditorModel,
original: this.modelRefs.original?.object.textEditorModel ?? undefined
};
}
registerOnTextChangedCallbacks(onTextChanged) {
this.onTextChanged = onTextChanged;
}
setModelRefDisposeTimeout(modelRefDisposeTimeout) {
this.modelRefDisposeTimeout = modelRefDisposeTimeout;
}
async init() {
const languageDef = this.config.languageDef;
if (languageDef) {
if (this.$type === 'extended') {
throw new Error('Language definition is not supported for extended editor apps where textmate is used.');
}
// register own language first
monaco.languages.register(languageDef.languageExtensionConfig);
const languageRegistered = monaco.languages.getLanguages().filter(x => x.id === languageDef.languageExtensionConfig.id);
if (languageRegistered.length === 0) {
// this is only meaningful for languages supported by monaco out of the box
monaco.languages.register({
id: languageDef.languageExtensionConfig.id
});
}
// apply monarch definitions
if (languageDef.monarchLanguage) {
monaco.languages.setMonarchTokensProvider(languageDef.languageExtensionConfig.id, languageDef.monarchLanguage);
}
if (languageDef.theme) {
monaco.editor.defineTheme(languageDef.theme.name, languageDef.theme.data);
monaco.editor.setTheme(languageDef.theme.name);
}
}
if (this.config.editorOptions?.['semanticHighlighting.enabled'] !== undefined) {
StandaloneServices.get(IConfigurationService).updateValue('editor.semanticHighlighting.enabled', this.config.editorOptions['semanticHighlighting.enabled'], ConfigurationTarget.USER);
}
// ensure proper default resources are initialized, uris have to be unique
const modified = {
text: this.config.codeResources?.modified?.text ?? '',
uri: this.config.codeResources?.modified?.uri ?? `default-uri-modified-${this.id}`,
enforceLanguageId: this.config.codeResources?.modified?.enforceLanguageId ?? undefined
};
this.modelRefs = {
modified: await this.buildModelReference(modified, this.logger)
};
if (this.config.useDiffEditor === true) {
const original = {
text: this.config.codeResources?.original?.text ?? '',
uri: this.config.codeResources?.original?.uri ?? `default-uri-original-${this.id}`,
enforceLanguageId: this.config.codeResources?.original?.enforceLanguageId ?? undefined
};
this.modelRefs.original = await this.buildModelReference(original, this.logger);
}
this.logger?.info('Init of EditorApp was completed.');
}
async createEditors(htmlContainer) {
if (this.config.useDiffEditor === true) {
this.diffEditor = monaco.editor.createDiffEditor(htmlContainer, this.config.diffEditorOptions);
const model = {
modified: this.modelRefs.modified.object.textEditorModel,
original: this.modelRefs.original.object.textEditorModel
};
this.diffEditor.setModel(model);
this.announceModelUpdate(model);
}
else {
const model = {
modified: this.modelRefs.modified.object.textEditorModel
};
this.editor = monaco.editor.create(htmlContainer, {
...this.config.editorOptions,
model: model.modified
});
this.announceModelUpdate(model);
}
}
async updateCodeResources(codeResources) {
let updateModified = false;
let updateOriginal = false;
if (codeResources?.modified !== undefined && codeResources.modified.uri !== this.modelRefs.modified.object.resource.path) {
this.modelDisposables.modified = this.modelRefs.modified;
this.modelRefs.modified = await this.buildModelReference(codeResources.modified, this.logger);
updateModified = true;
}
if (codeResources?.original !== undefined && codeResources.original.uri !== this.modelRefs.original?.object.resource.path) {
this.modelDisposables.original = this.modelRefs.original;
this.modelRefs.original = await this.buildModelReference(codeResources.original, this.logger);
updateOriginal = true;
}
if (this.config.useDiffEditor === true) {
if (updateModified && updateOriginal) {
const model = {
modified: this.modelRefs.modified.object.textEditorModel,
original: this.modelRefs.original.object.textEditorModel
};
this.diffEditor?.setModel(model);
this.announceModelUpdate(model);
}
else {
this.logger?.info('Diff Editor: Code resources were not updated. They are ether unchanged or undefined.');
}
}
else {
if (updateModified) {
const model = {
modified: this.modelRefs.modified.object.textEditorModel
};
this.editor?.setModel(model.modified);
this.announceModelUpdate(model);
}
else {
this.logger?.info('Editor: Code resources were not updated. They are either unchanged or undefined.');
}
}
await this.disposeModelRefs();
}
async buildModelReference(codeContent, logger) {
const code = codeContent.text;
const modelRef = await createModelReference(vscode.Uri.parse(codeContent.uri), code);
// update the text if different
if (modelRef.object.textEditorModel?.getValue() !== code) {
modelRef.object.textEditorModel?.setValue(code);
}
const enforceLanguageId = codeContent.enforceLanguageId;
if (enforceLanguageId !== undefined) {
modelRef.object.setLanguageId(enforceLanguageId);
logger?.info(`Main languageId is enforced: ${enforceLanguageId}`);
}
return modelRef;
}
;
announceModelUpdate(textModels) {
if (this.onTextChanged !== undefined) {
let changed = false;
if (textModels.modified !== undefined && textModels.modified !== null) {
const old = this.textChangedDiposeables.modified;
this.textChangedDiposeables.modified = textModels.modified.onDidChangeContent(() => {
didModelContentChange(textModels, this.onTextChanged);
});
old?.dispose();
changed = true;
}
if (textModels.original !== undefined && textModels.original !== null) {
const old = this.textChangedDiposeables.original;
this.textChangedDiposeables.original = textModels.original.onDidChangeContent(() => {
didModelContentChange(textModels, this.onTextChanged);
});
old?.dispose();
changed = true;
}
if (changed) {
// do it initially
didModelContentChange(textModels, this.onTextChanged);
}
}
}
async dispose() {
if (this.editor) {
this.editor.dispose();
this.editor = undefined;
}
if (this.diffEditor) {
this.diffEditor.dispose();
this.diffEditor = undefined;
}
this.textChangedDiposeables.modified?.dispose();
this.textChangedDiposeables.original?.dispose();
await this.disposeModelRefs();
}
async disposeModelRefs() {
const diposeRefs = () => {
if (this.logger?.getLevel() === LogLevel.Debug) {
const models = monaco.editor.getModels();
this.logger.debug('Current model URIs:');
models.forEach((model, _index) => {
this.logger?.debug(`${model.uri.toString()}`);
});
}
if (this.modelDisposables.modified !== undefined && !this.modelDisposables.modified.object.isDisposed()) {
this.modelDisposables.modified.dispose();
this.modelDisposables.modified = undefined;
}
if (this.modelDisposables.original !== undefined && !this.modelDisposables.original.object.isDisposed()) {
this.modelDisposables.original.dispose();
this.modelDisposables.original = undefined;
}
if (this.logger?.getLevel() === LogLevel.Debug) {
if (this.modelDisposables.modified === undefined && this.modelDisposables.original === undefined) {
this.logger.debug('All model references are disposed.');
}
else {
this.logger.debug('Model references are still available.');
}
}
};
if (this.modelRefDisposeTimeout > 0) {
this.logger?.debug('Using async dispose of model references');
await new Promise(resolve => setTimeout(() => {
diposeRefs();
resolve();
}, this.modelRefDisposeTimeout));
}
else {
diposeRefs();
}
}
updateLayout() {
if (this.config.useDiffEditor ?? false) {
this.diffEditor?.layout();
}
else {
this.editor?.layout();
}
}
}
export const verifyUrlOrCreateDataUrl = (input) => {
if (input instanceof URL) {
return input.href;
}
else {
const bytes = new TextEncoder().encode(input);
const binString = Array.from(bytes, (b) => String.fromCodePoint(b)).join('');
const base64 = btoa(binString);
return new URL(`data:text/plain;base64,${base64}`).href;
}
};
export const didModelContentChange = (textModels, onTextChanged) => {
const modified = textModels.modified?.getValue() ?? '';
const original = textModels.original?.getValue() ?? '';
onTextChanged?.({
modified,
original
});
};
//# sourceMappingURL=editorApp.js.map