UNPKG

monaco-editor-wrapper

Version:
394 lines (340 loc) 15.3 kB
/* -------------------------------------------------------------------------------------------- * 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, type ITextFileEditorModel } from '@codingame/monaco-vscode-api/monaco'; import { ConfigurationTarget, IConfigurationService, StandaloneServices } from '@codingame/monaco-vscode-api'; import type { IReference } from '@codingame/monaco-vscode-editor-service-override'; import type { Logger } from 'monaco-languageclient/tools'; import type { OverallConfigType } from './vscode/services.js'; export interface ModelRefs { modified: IReference<ITextFileEditorModel>; original?: IReference<ITextFileEditorModel>; } export interface TextModels { modified?: monaco.editor.ITextModel | null; original?: monaco.editor.ITextModel | null; } export interface TextContents { modified?: string; original?: string; } export interface CodeContent { text: string; uri: string; enforceLanguageId?: string; } export interface CodeResources { modified?: CodeContent; original?: CodeContent; } export interface CallbackDisposeable { modified?: monaco.IDisposable; original?: monaco.IDisposable; } export interface DisposableModelRefs { modified?: IReference<ITextFileEditorModel>; original?: IReference<ITextFileEditorModel>; } export interface EditorAppConfig { codeResources?: CodeResources; useDiffEditor?: boolean; domReadOnly?: boolean; readOnly?: boolean; overrideAutomaticLayout?: boolean; editorOptions?: monaco.editor.IStandaloneEditorConstructionOptions; diffEditorOptions?: monaco.editor.IStandaloneDiffEditorConstructionOptions; monacoWorkerFactory?: (logger?: Logger) => void; languageDef?: { languageExtensionConfig: monaco.languages.ILanguageExtensionPoint; monarchLanguage?: monaco.languages.IMonarchLanguage; theme?: { name: monaco.editor.BuiltinTheme | string; data: monaco.editor.IStandaloneThemeData; } } } /** * This is the base class for both Monaco Ediotor Apps: * - EditorAppClassic * - EditorAppExtended * * It provides the generic functionality for both implementations. */ export class EditorApp { private $type: OverallConfigType; private id: string; private config: EditorAppConfig; protected logger: Logger | undefined; private editor: monaco.editor.IStandaloneCodeEditor | undefined; private diffEditor: monaco.editor.IStandaloneDiffEditor | undefined; private modelRefs: ModelRefs; private onTextChanged?: (textChanges: TextContents) => void; private textChangedDiposeables: CallbackDisposeable = {}; private modelDisposables: DisposableModelRefs = {}; private modelRefDisposeTimeout = -1; constructor($type: OverallConfigType, id: string, userAppConfig?: EditorAppConfig, logger?: 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(): EditorAppConfig { return this.config; } haveEditor() { return this.editor !== undefined || this.diffEditor !== undefined; } getEditor(): monaco.editor.IStandaloneCodeEditor | undefined { return this.editor; } getDiffEditor(): monaco.editor.IStandaloneDiffEditor | undefined { return this.diffEditor; } getTextModels(): TextModels { return { modified: this.modelRefs.modified.object.textEditorModel, original: this.modelRefs.original?.object.textEditorModel ?? undefined }; } registerOnTextChangedCallbacks(onTextChanged?: (textChanges: TextContents) => void) { this.onTextChanged = onTextChanged; } public setModelRefDisposeTimeout(modelRefDisposeTimeout: number) { this.modelRefDisposeTimeout = modelRefDisposeTimeout; } async init(): Promise<void> { 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: HTMLElement): Promise<void> { 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?: CodeResources): Promise<void> { 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: CodeContent, logger?: Logger): Promise<IReference<ITextFileEditorModel>> { 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; }; private announceModelUpdate(textModels: 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<void>(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: string | URL) => { 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: TextModels, onTextChanged?: (textChanges: TextContents) => void) => { const modified = textModels.modified?.getValue() ?? ''; const original = textModels.original?.getValue() ?? ''; onTextChanged?.({ modified, original }); };