Skip to content
This repository has been archived by the owner on Mar 22, 2024. It is now read-only.

Commit

Permalink
Allow to init and start separately. Enforce dipose before start
Browse files Browse the repository at this point in the history
  • Loading branch information
kaisalmen committed Nov 8, 2023
1 parent 6481122 commit d760475
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 64 deletions.
1 change: 1 addition & 0 deletions packages/examples/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
17 changes: 13 additions & 4 deletions packages/examples/src/langium/wrapperLangium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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);
Expand All @@ -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!);
Expand All @@ -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;
}
};

Expand All @@ -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 = () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/examples/src/wrapperAdvanced.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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);
Expand Down
82 changes: 52 additions & 30 deletions packages/monaco-editor-react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export class MonacoEditorReactComp<T extends MonacoEditorProps = MonacoEditorPro
private wrapper: MonacoEditorLanguageClientWrapper = new MonacoEditorLanguageClientWrapper();
private containerElement?: HTMLDivElement;
private _subscription: IDisposable | null = null;
private isStarting?: Promise<void>;
private isRestaring?: Promise<void>;
private started: (value: void | PromiseLike<void>) => void;

constructor(props: T) {
super(props);
Expand All @@ -27,9 +28,10 @@ export class MonacoEditorReactComp<T extends MonacoEditorProps = MonacoEditorPro
await this.handleReinit();
}

private async handleReinit() {
protected async handleReinit() {
await this.destroyMonaco();
await this.initMonaco();
await this.startMonaco();
}

override async componentDidUpdate(prevProps: T) {
Expand Down Expand Up @@ -85,58 +87,78 @@ export class MonacoEditorReactComp<T extends MonacoEditorProps = MonacoEditorPro
this.destroyMonaco();
}

private assignRef = (component: HTMLDivElement) => {
protected assignRef = (component: HTMLDivElement) => {
this.containerElement = component;
};

private async destroyMonaco(): Promise<void> {
protected async destroyMonaco(): Promise<void> {
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) {
this._subscription.dispose();
}
}

private async initMonaco() {
protected async initMonaco() {
const {
userConfig
} = this.props;

// block "destroyMonaco" until start is complete
this.isRestaring = new Promise<void>((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();
}
}

Expand Down
11 changes: 4 additions & 7 deletions packages/monaco-editor-wrapper/src/languageClientWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}

Expand Down
47 changes: 31 additions & 16 deletions packages/monaco-editor-wrapper/src/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -158,6 +172,7 @@ export class MonacoEditorLanguageClientWrapper {
else {
await Promise.resolve('Monaco editor has been disposed.');
}
this.initDone = false;
}

updateLayout() {
Expand Down
18 changes: 12 additions & 6 deletions packages/monaco-editor-wrapper/test/languageClientWrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
Expand All @@ -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();
});

Expand All @@ -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.',
Expand All @@ -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.',
Expand Down
Loading

0 comments on commit d760475

Please sign in to comment.