diff --git a/packages/examples/src/common.ts b/packages/examples/src/common.ts index bd3e491..74cc509 100644 --- a/packages/examples/src/common.ts +++ b/packages/examples/src/common.ts @@ -38,6 +38,7 @@ export const disposeEditor = async (useDiffEditor: boolean) => { }; const restartEditor = async (userConfig: UserConfig, htmlElement: HTMLElement | null) => { + await wrapper.dispose(); await wrapper.start(userConfig, htmlElement); logEditorInfo(userConfig); }; diff --git a/packages/examples/src/langium/wrapperLangium.ts b/packages/examples/src/langium/wrapperLangium.ts index 9230206..89ae7d3 100644 --- a/packages/examples/src/langium/wrapperLangium.ts +++ b/packages/examples/src/langium/wrapperLangium.ts @@ -11,6 +11,7 @@ import { buildWorkerDefinition } from 'monaco-editor-workers'; buildWorkerDefinition('../../../node_modules/monaco-editor-workers/dist/workers/', new URL('', window.location.href).href, false); let wrapper: MonacoEditorLanguageClientWrapper | undefined; +let extended = false; const htmlElement = document.getElementById('monaco-editor-root'); export const run = async () => { @@ -26,7 +27,9 @@ export const run = async () => { export const startLangiumClientExtended = async () => { try { if (checkStarted()) return; - disableButton('button-start-classic'); + extended = true; + disableButton('button-start-classic', true); + disableButton('button-start-extended', true); const config = await setupLangiumClientExtended(); wrapper = new MonacoEditorLanguageClientWrapper(); wrapper.start(config, htmlElement); @@ -38,7 +41,8 @@ export const startLangiumClientExtended = async () => { export const startLangiumClientClassic = async () => { try { if (checkStarted()) return; - disableButton('button-start-extended'); + disableButton('button-start-classic', true); + disableButton('button-start-extended', true); const config = await setupLangiumClientClassic(); wrapper = new MonacoEditorLanguageClientWrapper(); await wrapper.start(config, htmlElement!); @@ -55,10 +59,10 @@ const checkStarted = () => { return false; }; -const disableButton = (id: string) => { +const disableButton = (id: string, disabled: boolean) => { const button = document.getElementById(id) as HTMLButtonElement; if (button !== null) { - button.disabled = true; + button.disabled = disabled; } }; @@ -67,6 +71,11 @@ export const disposeEditor = async () => { wrapper.reportStatus(); await wrapper.dispose(); wrapper = undefined; + if (extended) { + disableButton('button-start-extended', false); + } else { + disableButton('button-start-classic', false); + } }; export const loadLangiumWorker = () => { diff --git a/packages/examples/src/wrapperAdvanced.ts b/packages/examples/src/wrapperAdvanced.ts index dcb4319..820041a 100644 --- a/packages/examples/src/wrapperAdvanced.ts +++ b/packages/examples/src/wrapperAdvanced.ts @@ -1,5 +1,6 @@ import getKeybindingsServiceOverride from '@codingame/monaco-vscode-keybindings-service-override'; import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js'; +import 'monaco-editor/esm/vs/language/typescript/monaco.contribution.js'; import { EditorAppConfigClassic, LanguageClientError, MonacoEditorLanguageClientWrapper, UserConfig } from 'monaco-editor-wrapper'; import { buildWorkerDefinition } from 'monaco-editor-workers'; @@ -114,6 +115,7 @@ const sleepOne = (milliseconds: number) => { setTimeout(async () => { alert(`Updating editors after ${milliseconds}ms`); + await wrapper42.dispose(); wrapper42Config.languageClientConfig = undefined; const appConfig42 = wrapper42Config.wrapperConfig.editorAppConfig as EditorAppConfigClassic; appConfig42.languageId = 'javascript'; @@ -129,6 +131,7 @@ const sleepOne = (milliseconds: number) => { codeOriginal: 'text 1234' }); + await wrapper44.dispose(); const appConfig44 = wrapper44Config.wrapperConfig.editorAppConfig as EditorAppConfigClassic; appConfig44.languageId = 'text/plain'; appConfig44.useDiffEditor = true; @@ -152,10 +155,10 @@ const sleepTwo = (milliseconds: number) => { setTimeout(async () => { alert(`Updating last editor after ${milliseconds}ms`); + await wrapper44.dispose(); const appConfig44 = wrapper44Config.wrapperConfig.editorAppConfig as EditorAppConfigClassic; appConfig44.useDiffEditor = false; appConfig44.theme = 'vs-dark'; - await wrapper44.start(wrapper44Config, document.getElementById('monaco-editor-root-44')); console.log('Restarted wrapper44.'); }, milliseconds); diff --git a/packages/monaco-editor-react/src/index.tsx b/packages/monaco-editor-react/src/index.tsx index 4ba8a8f..95af043 100644 --- a/packages/monaco-editor-react/src/index.tsx +++ b/packages/monaco-editor-react/src/index.tsx @@ -16,7 +16,8 @@ export class MonacoEditorReactComp; + private isRestaring?: Promise; + private started: (value: void | PromiseLike) => void; constructor(props: T) { super(props); @@ -27,9 +28,10 @@ export class MonacoEditorReactComp { + protected assignRef = (component: HTMLDivElement) => { this.containerElement = component; }; - private async destroyMonaco(): Promise { + protected async destroyMonaco(): Promise { if (this.wrapper) { - await this.isStarting; + if (this.isRestaring) { + await this.isRestaring; + } try { await this.wrapper.dispose(); } catch { - // This is fine - // Sometimes the language client throws an error during disposal - // This should not prevent us from continue working + // The language client may throw an error during disposal. + // This should not prevent us from continue working. } } if (this._subscription) { @@ -105,38 +108,57 @@ export class MonacoEditorReactComp((resolve) => { + this.started = resolve; + }); + await this.wrapper.init(userConfig); + } + + protected async startMonaco() { const { className, - userConfig, - onTextChanged, onLoad, } = this.props; if (this.containerElement) { this.containerElement.className = className ?? ''; - this.isStarting = this.wrapper.start(userConfig, this.containerElement); - await this.isStarting; + await this.wrapper.startNoInit(this.containerElement); + this.started(); + this.isRestaring = undefined; // once awaiting isStarting is done onLoad is called if available - onLoad && onLoad(); - - if (onTextChanged) { - const model = this.wrapper.getModel(); - if (model) { - const verifyModelContent = () => { - const modelText = model.getValue(); - onTextChanged(modelText, modelText !== userConfig.wrapperConfig.editorAppConfig.code); - }; - - this._subscription = model.onDidChangeContent(() => { - verifyModelContent(); - }); - // do it initially - verifyModelContent(); - } - } + onLoad?.(); + + this.handleOnTextChanged(); + } + } + + private handleOnTextChanged() { + const { + userConfig, + onTextChanged + } = this.props; + if (!onTextChanged) return; + + const model = this.wrapper.getModel(); + if (model) { + const verifyModelContent = () => { + const modelText = model.getValue(); + onTextChanged(modelText, modelText !== userConfig.wrapperConfig.editorAppConfig.code); + }; + + this._subscription = model.onDidChangeContent(() => { + verifyModelContent(); + }); + // do it initially + verifyModelContent(); } } diff --git a/packages/monaco-editor-wrapper/src/languageClientWrapper.ts b/packages/monaco-editor-wrapper/src/languageClientWrapper.ts index d9d1246..5fafc7a 100644 --- a/packages/monaco-editor-wrapper/src/languageClientWrapper.ts +++ b/packages/monaco-editor-wrapper/src/languageClientWrapper.ts @@ -73,10 +73,10 @@ export class LanguageClientWrapper { private languageClientConfig?: LanguageClientConfig; private worker: Worker | undefined; private languageId: string; - private name; + private name?: string; private logger: Logger | undefined; - constructor(languageId: string, languageClientConfig?: LanguageClientConfig, logger?: Logger) { + init(languageId: string, languageClientConfig?: LanguageClientConfig, logger?: Logger) { this.languageId = languageId; if (languageClientConfig) { this.languageClientConfig = languageClientConfig; @@ -292,11 +292,8 @@ export class LanguageClientWrapper { } } else { - const languageClientError: LanguageClientError = { - message: `languageClientWrapper (${this.name}): Unable to dispose monaco-languageclient: It is not yet started.`, - error: 'No error was provided.' - }; - return Promise.reject(languageClientError); + // disposing the languageclient if it does not exist is considered ok + return Promise.resolve(); } } diff --git a/packages/monaco-editor-wrapper/src/wrapper.ts b/packages/monaco-editor-wrapper/src/wrapper.ts index f24dd13..92e1f38 100644 --- a/packages/monaco-editor-wrapper/src/wrapper.ts +++ b/packages/monaco-editor-wrapper/src/wrapper.ts @@ -29,18 +29,34 @@ export class MonacoEditorLanguageClientWrapper { private id: string; private editorApp: EditorAppClassic | EditorAppExtended | undefined; - private languageClientWrapper: LanguageClientWrapper; + private languageClientWrapper: LanguageClientWrapper = new LanguageClientWrapper(); private serviceConfig: InitializeServiceConfig; private logger: Logger; + private initDone = false; - private init(userConfig: UserConfig) { + async init(userConfig: UserConfig) { if (userConfig.wrapperConfig.editorAppConfig.useDiffEditor && !userConfig.wrapperConfig.editorAppConfig.codeOriginal) { throw new Error('Use diff editor was used without a valid config.'); } + // Always dispose old instances before start + this.editorApp?.disposeApp(); this.id = userConfig.id ?? Math.floor(Math.random() * 101).toString(); this.logger = new Logger(userConfig.loggerConfig); this.serviceConfig = userConfig.wrapperConfig.serviceConfig ?? {}; + + if (userConfig.wrapperConfig.editorAppConfig.$type === 'classic') { + this.editorApp = new EditorAppClassic(this.id, userConfig, this.logger); + } else { + this.editorApp = new EditorAppExtended(this.id, userConfig, this.logger); + } + // editorApps init their own service thats why they have to be created first + await this.initServices(); + + this.languageClientWrapper.init(this.editorApp.getConfig().languageId, + userConfig.languageClientConfig, this.logger); + + this.initDone = true; } private async initServices() { @@ -68,27 +84,25 @@ export class MonacoEditorLanguageClientWrapper { } async start(userConfig: UserConfig, htmlElement: HTMLElement | null) { + if (!this.initDone) { + await this.init(userConfig); + } else { + throw new Error('init was already performed. Please call startNoInit()'); + } + await this.startNoInit(htmlElement); + } + + async startNoInit(htmlElement: HTMLElement | null) { if (!htmlElement) { throw new Error('No HTMLElement provided for monaco-editor.'); } - // Always dispose old instances before start - this.editorApp?.disposeApp(); - - this.init(userConfig); - - if (userConfig.wrapperConfig.editorAppConfig.$type === 'classic') { - this.editorApp = new EditorAppClassic(this.id, userConfig, this.logger); - } else { - this.editorApp = new EditorAppExtended(this.id, userConfig, this.logger); + if (!this.initDone) { + throw new Error('No init was performed. Please call init() before startNoInit()'); } - await this.initServices(); - - this.languageClientWrapper = new LanguageClientWrapper(this.editorApp.getConfig().languageId, - userConfig.languageClientConfig, this.logger); this.logger.info(`Starting monaco-editor (${this.id})`); await this.editorApp?.init(); - await this.editorApp.createEditors(htmlElement); + await this.editorApp?.createEditors(htmlElement); if (this.languageClientWrapper.haveLanguageClientConfig()) { await this.languageClientWrapper.start(); @@ -158,6 +172,7 @@ export class MonacoEditorLanguageClientWrapper { else { await Promise.resolve('Monaco editor has been disposed.'); } + this.initDone = false; } updateLayout() { diff --git a/packages/monaco-editor-wrapper/test/languageClientWrapper.test.ts b/packages/monaco-editor-wrapper/test/languageClientWrapper.test.ts index 2cc62ce..745966c 100644 --- a/packages/monaco-editor-wrapper/test/languageClientWrapper.test.ts +++ b/packages/monaco-editor-wrapper/test/languageClientWrapper.test.ts @@ -4,14 +4,16 @@ import { LanguageClientConfig, LanguageClientWrapper } from 'monaco-editor-wrapp describe('Test LanguageClientWrapper', () => { test('Not Running after construction', () => { - const languageClientWrapper = new LanguageClientWrapper('my-lang'); + const languageClientWrapper = new LanguageClientWrapper(); + languageClientWrapper.init('my-lang'); expect(languageClientWrapper.haveLanguageClient()).toBeFalsy(); expect(languageClientWrapper.haveLanguageClientConfig()).toBeFalsy(); expect(languageClientWrapper.isStarted()).toBeFalsy(); }); test('Constructor: no config', async () => { - const languageClientWrapper = new LanguageClientWrapper('my-lang'); + const languageClientWrapper = new LanguageClientWrapper(); + languageClientWrapper.init('my-lang'); expect(async () => { await languageClientWrapper.start(); }).rejects.toEqual({ @@ -37,7 +39,8 @@ describe('Test LanguageClientWrapper', () => { }); // setup the wrapper - const languageClientWrapper = new LanguageClientWrapper('my-lang', { + const languageClientWrapper = new LanguageClientWrapper(); + languageClientWrapper.init('my-lang', { options: { $type: 'WorkerDirect', worker @@ -61,7 +64,8 @@ describe('Test LanguageClientWrapper', () => { url: 'ws://localhost:12345/Tester' } }; - const languageClientWrapper = new LanguageClientWrapper('my-lang', languageClientConfig); + const languageClientWrapper = new LanguageClientWrapper(); + languageClientWrapper.init('my-lang', languageClientConfig); expect(languageClientWrapper.haveLanguageClientConfig()).toBeTruthy(); }); @@ -73,7 +77,8 @@ describe('Test LanguageClientWrapper', () => { name: 'test-unreachable' } }; - const languageClientWrapper = new LanguageClientWrapper('my-lang', languageClientConfig); + const languageClientWrapper = new LanguageClientWrapper(); + languageClientWrapper.init('my-lang', languageClientConfig); expect(languageClientWrapper.haveLanguageClientConfig()).toBeTruthy(); await expect(languageClientWrapper.start()).rejects.toEqual({ message: 'languageClientWrapper (test-unreachable): Websocket connection failed.', @@ -100,7 +105,8 @@ describe('Test LanguageClientWrapper', () => { type: 'classic' } }; - const languageClientWrapper = new LanguageClientWrapper('my-lang', languageClientConfig); + const languageClientWrapper = new LanguageClientWrapper(); + languageClientWrapper.init('my-lang', languageClientConfig); expect(languageClientWrapper.haveLanguageClientConfig()).toBeTruthy(); await expect(languageClientWrapper.start()).rejects.toEqual({ message: 'languageClientWrapper (unnamed): Illegal worker configuration detected. Potentially the url is wrong.', diff --git a/packages/monaco-editor-wrapper/test/wrapper.test.ts b/packages/monaco-editor-wrapper/test/wrapper.test.ts index ba9614b..7776586 100644 --- a/packages/monaco-editor-wrapper/test/wrapper.test.ts +++ b/packages/monaco-editor-wrapper/test/wrapper.test.ts @@ -37,4 +37,22 @@ describe('Test MonacoEditorLanguageClientWrapper', () => { await wrapper.start(createBaseConfig('classic'), null); }).rejects.toThrowError('No HTMLElement provided for monaco-editor.'); }); + + test('Expected throw: Start without init', async () => { + createMonacoEditorDiv(); + const wrapper = new MonacoEditorLanguageClientWrapper(); + await expect(async () => { + await wrapper.startNoInit(document.getElementById('monaco-editor-root')); + }).rejects.toThrowError('No init was performed. Please call init() before startNoInit()'); + }); + + test('Expected throw: Call normal start with prior init', async () => { + createMonacoEditorDiv(); + const wrapper = new MonacoEditorLanguageClientWrapper(); + await expect(async () => { + const config = createBaseConfig('classic'); + await wrapper.init(config); + await wrapper.start(config, document.getElementById('monaco-editor-root')); + }).rejects.toThrowError('init was already performed. Please call startNoInit()'); + }); });