diff --git a/packages/examples/src/common.ts b/packages/examples/src/common.ts index d3d08b5..c012aa3 100644 --- a/packages/examples/src/common.ts +++ b/packages/examples/src/common.ts @@ -14,7 +14,7 @@ export const startEditor = async (userConfig: UserConfig, code: string, codeOrig }; export const updateModel = async (modelUpdate: ModelUpdate) => { - if (wrapper.getMonacoEditorApp()?.getAppConfig().useDiffEditor) { + if (wrapper.getMonacoEditorApp()?.getConfig().useDiffEditor) { await wrapper?.updateDiffModel(modelUpdate); } else { await wrapper?.updateModel(modelUpdate); diff --git a/packages/examples/src/wrapperTs.ts b/packages/examples/src/wrapperTs.ts index 09b443e..997f498 100644 --- a/packages/examples/src/wrapperTs.ts +++ b/packages/examples/src/wrapperTs.ts @@ -58,7 +58,7 @@ try { swapEditors(userConfig, code, codeOriginal); }); document.querySelector('#button-swap-code')?.addEventListener('click', () => { - if (wrapper.getMonacoEditorApp()?.getAppConfig().codeUri === codeUri) { + if (wrapper.getMonacoEditorApp()?.getConfig().codeUri === codeUri) { updateModel({ code: codeOriginal, uri: codeOriginalUri, @@ -73,7 +73,7 @@ try { } }); document.querySelector('#button-dispose')?.addEventListener('click', async () => { - if (wrapper.getMonacoEditorApp()?.getAppConfig().codeUri === codeUri) { + if (wrapper.getMonacoEditorApp()?.getConfig().codeUri === codeUri) { code = await disposeEditor(userConfig); } else { codeOriginal = await disposeEditor(userConfig); diff --git a/packages/monaco-editor-wrapper/src/editor.ts b/packages/monaco-editor-wrapper/src/editorAppBase.ts similarity index 64% rename from packages/monaco-editor-wrapper/src/editor.ts rename to packages/monaco-editor-wrapper/src/editorAppBase.ts index cd4dbdc..bd8dd7e 100644 --- a/packages/monaco-editor-wrapper/src/editor.ts +++ b/packages/monaco-editor-wrapper/src/editorAppBase.ts @@ -4,12 +4,14 @@ import { editor, Uri } from 'monaco-editor/esm/vs/editor/editor.api.js'; import { createConfiguredEditor, createConfiguredDiffEditor, createModelReference, ITextFileEditorModel } from 'vscode/monaco'; import { IReference } from 'vscode/service-override/editor'; import { ModelUpdate, UserConfig, WrapperConfig } from './wrapper.js'; +import { EditorAppConfigClassic } from './editorAppClassic.js'; +import { EditorAppConfigVscodeApi } from './editorAppVscodeApi.js'; export type VscodeUserConfiguration = { json?: string; } -export type EditorAppConfig = { +export type EditorAppBaseConfig = { languageId: string; code: string; codeUri?: string; @@ -20,26 +22,30 @@ export type EditorAppConfig = { export type EditorAppType = 'vscodeApi' | 'classic'; +/** + * This is the base class for both Monaco Ediotor Apps: + * - EditorAppClassic + * - EditorAppVscodeApi + * + * It provides the generic functionality for both implementations. + */ export abstract class EditorAppBase { private id: string; - protected appConfig: EditorAppConfig; - protected editor: editor.IStandaloneCodeEditor | undefined; - protected diffEditor: editor.IStandaloneDiffEditor | undefined; - - protected editorOptions: editor.IStandaloneEditorConstructionOptions; - protected diffEditorOptions: editor.IStandaloneDiffEditorConstructionOptions; + private editor: editor.IStandaloneCodeEditor | undefined; + private diffEditor: editor.IStandaloneDiffEditor | undefined; private modelRef: IReference | undefined; private modelOriginalRef: IReference | undefined; - constructor(id: string, userConfig: UserConfig) { + constructor(id: string) { this.id = id; - console.log(`Starting monaco-editor (${this.id})`); + } + buildConfig(userConfig: UserConfig) { const userAppConfig = userConfig.wrapperConfig.editorAppConfig; - this.appConfig = { + return { languageId: userAppConfig.languageId, code: userAppConfig.code ?? '', codeOriginal: userAppConfig.codeOriginal ?? '', @@ -61,14 +67,14 @@ export abstract class EditorAppBase { return this.diffEditor; } - async createEditors(container: HTMLElement): Promise { - if (this.appConfig.useDiffEditor) { - this.diffEditor = createConfiguredDiffEditor(container!, this.diffEditorOptions); - await this.updateDiffEditorModel(); - } else { - this.editor = createConfiguredEditor(container!, this.editorOptions); - await this.updateEditorModel(); - } + protected async createEditor(container: HTMLElement, editorOptions?: editor.IStandaloneEditorConstructionOptions): Promise { + this.editor = createConfiguredEditor(container!, editorOptions); + await this.updateEditorModel(); + } + + protected async createDiffEditor(container: HTMLElement, diffEditorOptions?: editor.IStandaloneDiffEditorConstructionOptions): Promise { + this.diffEditor = createConfiguredDiffEditor(container!, diffEditorOptions); + await this.updateDiffEditorModel(); } disposeEditor() { @@ -90,7 +96,7 @@ export abstract class EditorAppBase { } getModel(original?: boolean): editor.ITextModel | undefined { - if (this.appConfig.useDiffEditor) { + if (this.getConfig().useDiffEditor) { return ((original === true) ? this.modelOriginalRef?.object.textEditorModel : this.modelRef?.object.textEditorModel) ?? undefined; } else { return this.modelRef?.object.textEditorModel ?? undefined; @@ -102,16 +108,17 @@ export abstract class EditorAppBase { return Promise.reject(new Error('You cannot update the editor model, because the regular editor is not configured.')); } - this.updateEditorConfig(modelUpdate); + this.updateAppConfig(modelUpdate); await this.updateEditorModel(); } private async updateEditorModel(): Promise { + const config = this.getConfig(); this.modelRef?.dispose(); const uri: Uri = this.getEditorUri('code'); - this.modelRef = await createModelReference(uri, this.appConfig.code) as unknown as IReference; - this.modelRef.object.setLanguageId(this.appConfig.languageId); + this.modelRef = await createModelReference(uri, config.code) as unknown as IReference; + this.modelRef.object.setLanguageId(config.languageId); if (this.editor) { this.editor.setModel(this.modelRef.object.textEditorModel); } @@ -122,11 +129,12 @@ export abstract class EditorAppBase { return Promise.reject(new Error('You cannot update the diff editor models, because the diffEditor is not configured.')); } - this.updateEditorConfig(modelUpdate); + this.updateAppConfig(modelUpdate); return this.updateDiffEditorModel(); } private async updateDiffEditorModel(): Promise { + const config = this.getConfig(); this.modelRef?.dispose(); this.modelOriginalRef?.dispose(); @@ -134,14 +142,14 @@ export abstract class EditorAppBase { const uriOriginal: Uri = this.getEditorUri('codeOriginal'); const promises = []; - promises.push(createModelReference(uri, this.appConfig.code)); - promises.push(createModelReference(uriOriginal, this.appConfig.codeOriginal)); + promises.push(createModelReference(uri, config.code)); + promises.push(createModelReference(uriOriginal, config.codeOriginal)); const refs = await Promise.all(promises); this.modelRef = refs[0] as unknown as IReference; - this.modelRef.object.setLanguageId(this.appConfig.languageId); + this.modelRef.object.setLanguageId(config.languageId); this.modelOriginalRef = refs[1] as unknown as IReference; - this.modelOriginalRef.object.setLanguageId(this.appConfig.languageId); + this.modelOriginalRef.object.setLanguageId(config.languageId); if (this.diffEditor && this.modelRef.object.textEditorModel !== null && this.modelOriginalRef.object.textEditorModel !== null) { this.diffEditor?.setModel({ @@ -151,49 +159,56 @@ export abstract class EditorAppBase { } } - private updateEditorConfig(modelUpdate: ModelUpdate) { + private updateAppConfig(modelUpdate: ModelUpdate) { + const config = this.getConfig(); if (modelUpdate.code !== undefined) { - this.appConfig.code = modelUpdate.code; + config.code = modelUpdate.code; } if (modelUpdate.languageId !== undefined) { - this.appConfig.languageId = modelUpdate.languageId; + config.languageId = modelUpdate.languageId; } if (modelUpdate.uri !== undefined) { - this.appConfig.codeUri = modelUpdate.uri; + config.codeUri = modelUpdate.uri; } if (modelUpdate.codeOriginal !== undefined) { - this.appConfig.codeOriginal = modelUpdate.codeOriginal; + config.codeOriginal = modelUpdate.codeOriginal; } if (modelUpdate.codeOriginalUri !== undefined) { - this.appConfig.codeOriginalUri = modelUpdate.codeOriginalUri; + config.codeOriginalUri = modelUpdate.codeOriginalUri; } } getEditorUri(uriType: 'code' | 'codeOriginal') { - const uri = uriType === 'code' ? this.appConfig.codeUri : this.appConfig.codeOriginalUri; + const config = this.getConfig(); + const uri = uriType === 'code' ? config.codeUri : config.codeOriginalUri; if (uri) { return Uri.parse(uri); } else { - return Uri.parse(`/tmp/model${uriType === 'codeOriginal' ? 'Original' : ''}${this.id}.${this.appConfig.languageId}`); + return Uri.parse(`/tmp/model${uriType === 'codeOriginal' ? 'Original' : ''}${this.id}.${config.languageId}`); } } updateLayout() { - if (this.appConfig.useDiffEditor) { + if (this.getConfig().useDiffEditor) { this.diffEditor?.layout(); } else { this.editor?.layout(); } } + updateMonacoEditorOptions(options: editor.IEditorOptions & editor.IGlobalEditorOptions) { + this.editor?.updateOptions(options); + } + abstract getAppType(): string; abstract init(): Promise; - abstract updateConfig(options: editor.IEditorOptions & editor.IGlobalEditorOptions | VscodeUserConfiguration): void; - abstract getAppConfig(): EditorAppConfig; + abstract createEditors(container: HTMLElement): Promise; + abstract updateEditorOptions(options: editor.IEditorOptions & editor.IGlobalEditorOptions | VscodeUserConfiguration): void; + abstract getConfig(): EditorAppConfigClassic | EditorAppConfigVscodeApi; } export const isVscodeApiEditorApp = (wrapperConfig: WrapperConfig) => { diff --git a/packages/monaco-editor-wrapper/src/editorClassic.ts b/packages/monaco-editor-wrapper/src/editorAppClassic.ts similarity index 53% rename from packages/monaco-editor-wrapper/src/editorClassic.ts rename to packages/monaco-editor-wrapper/src/editorAppClassic.ts index 342f54c..b8f5e54 100644 --- a/packages/monaco-editor-wrapper/src/editorClassic.ts +++ b/packages/monaco-editor-wrapper/src/editorAppClassic.ts @@ -1,4 +1,4 @@ -import { EditorAppBase, EditorAppConfig, EditorAppType } from './editor.js'; +import { EditorAppBase, EditorAppBaseConfig, EditorAppType } from './editorAppBase.js'; import { editor, languages } from 'monaco-editor/esm/vs/editor/editor.api.js'; import { UserConfig } from './wrapper.js'; /** @@ -15,7 +15,7 @@ export type MonacoLanguageExtensionConfig = { mimetypes?: string[]; } -export type EditorAppConfigClassic = EditorAppConfig & { +export type EditorAppConfigClassic = EditorAppBaseConfig & { editorAppType: 'classic'; automaticLayout?: boolean; theme?: string; @@ -26,15 +26,24 @@ export type EditorAppConfigClassic = EditorAppConfig & { themeData?: editor.IStandaloneThemeData; }; +/** + * The classic monaco-editor app uses the classic monaco-editor configuration. + */ export class EditorAppClassic extends EditorAppBase { + private editorOptions: editor.IStandaloneEditorConstructionOptions; + private diffEditorOptions: editor.IStandaloneDiffEditorConstructionOptions; + + private config: EditorAppConfigClassic; + constructor(id: string, userConfig: UserConfig) { - super(id, userConfig); + super(id); + this.config = this.buildConfig(userConfig) as EditorAppConfigClassic; const userInput = userConfig.wrapperConfig.editorAppConfig as EditorAppConfigClassic; // default to vs-light - this.getAppConfig().theme = userInput.theme ?? 'vs-light'; + this.config.theme = userInput.theme ?? 'vs-light'; // default to true - this.getAppConfig().automaticLayout = userInput.automaticLayout ?? true; + this.config.automaticLayout = userInput.automaticLayout ?? true; this.editorOptions = userInput.editorOptions ?? {}; this.editorOptions.automaticLayout = userInput.automaticLayout ?? true; @@ -42,50 +51,58 @@ export class EditorAppClassic extends EditorAppBase { this.diffEditorOptions = userInput.diffEditorOptions ?? {}; this.diffEditorOptions.automaticLayout = userInput.automaticLayout ?? true; - this.getAppConfig().languageExtensionConfig = userInput.languageExtensionConfig ?? undefined; - this.getAppConfig().languageDef = userInput.languageDef ?? undefined; - this.getAppConfig().themeData = userInput.themeData ?? undefined; + this.config.languageExtensionConfig = userInput.languageExtensionConfig ?? undefined; + this.config.languageDef = userInput.languageDef ?? undefined; + this.config.themeData = userInput.themeData ?? undefined; } getAppType(): EditorAppType { return 'classic'; } - getAppConfig(): EditorAppConfigClassic { - return this.appConfig as EditorAppConfigClassic; + getConfig(): EditorAppConfigClassic { + return this.config; + } + + async createEditors(container: HTMLElement): Promise { + if (this.config.useDiffEditor) { + await this.createDiffEditor(container, this.diffEditorOptions); + } else { + await this.createEditor(container, this.editorOptions); + } } async init() { // register own language first - const extLang = this.getAppConfig().languageExtensionConfig; + const extLang = this.config.languageExtensionConfig; if (extLang) { languages.register(extLang); } - const languageRegistered = languages.getLanguages().filter(x => x.id === this.appConfig.languageId); + const languageRegistered = languages.getLanguages().filter(x => x.id === this.config.languageId); if (languageRegistered.length === 0) { // this is only meaningful for languages supported by monaco out of the box languages.register({ - id: this.appConfig.languageId + id: this.config.languageId }); } // apply monarch definitions - const tokenProvider = this.getAppConfig().languageDef; + const tokenProvider = this.config.languageDef; if (tokenProvider) { - languages.setMonarchTokensProvider(this.appConfig.languageId, tokenProvider); + languages.setMonarchTokensProvider(this.config.languageId, tokenProvider); } - const themeData = this.getAppConfig().themeData; + const themeData = this.config.themeData; if (themeData) { - editor.defineTheme(this.getAppConfig().theme!, themeData); + editor.defineTheme(this.config.theme!, themeData); } - editor.setTheme(this.getAppConfig().theme!); + editor.setTheme(this.config.theme!); console.log('Init of MonacoConfig was completed.'); return Promise.resolve(); } - async updateConfig(options: editor.IEditorOptions & editor.IGlobalEditorOptions) { - this.editor?.updateOptions(options); + async updateEditorOptions(options: editor.IEditorOptions & editor.IGlobalEditorOptions) { + this.updateMonacoEditorOptions(options); } } diff --git a/packages/monaco-editor-wrapper/src/editorVscodeApi.ts b/packages/monaco-editor-wrapper/src/editorAppVscodeApi.ts similarity index 52% rename from packages/monaco-editor-wrapper/src/editorVscodeApi.ts rename to packages/monaco-editor-wrapper/src/editorAppVscodeApi.ts index 33e0183..17d258d 100644 --- a/packages/monaco-editor-wrapper/src/editorVscodeApi.ts +++ b/packages/monaco-editor-wrapper/src/editorAppVscodeApi.ts @@ -1,39 +1,53 @@ -import { EditorAppBase, EditorAppConfig, EditorAppType, VscodeUserConfiguration } from './editor.js'; +import { EditorAppBase, EditorAppBaseConfig, EditorAppType, VscodeUserConfiguration } from './editorAppBase.js'; import { updateUserConfiguration } from 'vscode/service-override/configuration'; import { registerExtension, IExtensionManifest, ExtensionHostKind } from 'vscode/extensions'; import 'vscode/default-extensions/theme-defaults'; import { UserConfig } from './wrapper.js'; -export type EditorAppConfigVscodeApi = EditorAppConfig & { +export type EditorAppConfigVscodeApi = EditorAppBaseConfig & { editorAppType: 'vscodeApi'; extension?: IExtensionManifest | object; extensionFilesOrContents?: Map; userConfiguration?: VscodeUserConfiguration; }; +/** + * The vscode-apo monaco-editor app uses vscode user and extension configuration for monaco-editor. + */ export class EditorAppVscodeApi extends EditorAppBase { + private config: EditorAppConfigVscodeApi; + constructor(id: string, userConfig: UserConfig) { - super(id, userConfig); + super(id); + this.config = this.buildConfig(userConfig) as EditorAppConfigVscodeApi; const userInput = userConfig.wrapperConfig.editorAppConfig as EditorAppConfigVscodeApi; - this.getAppConfig().userConfiguration = userInput.userConfiguration ?? undefined; - this.getAppConfig().extension = userInput.extension ?? undefined; - this.getAppConfig().extensionFilesOrContents = userInput.extensionFilesOrContents ?? undefined; + this.config.userConfiguration = userInput.userConfiguration ?? undefined; + this.config.extension = userInput.extension ?? undefined; + this.config.extensionFilesOrContents = userInput.extensionFilesOrContents ?? undefined; } getAppType(): EditorAppType { return 'vscodeApi'; } - getAppConfig(): EditorAppConfigVscodeApi { - return this.appConfig as EditorAppConfigVscodeApi; + getConfig(): EditorAppConfigVscodeApi { + return this.config; + } + + async createEditors(container: HTMLElement): Promise { + if (this.config.useDiffEditor) { + this.createDiffEditor(container); + } else { + this.createEditor(container); + } } async init() { - if (this.getAppConfig().extension) { - const extension = this.getAppConfig().extension as IExtensionManifest; + if (this.config.extension) { + const extension = this.config.extension as IExtensionManifest; const { registerFileUrl } = registerExtension(extension, ExtensionHostKind.LocalProcess); - const extensionFilesOrContents = this.getAppConfig().extensionFilesOrContents; + const extensionFilesOrContents = this.config.extensionFilesOrContents; if (extensionFilesOrContents) { for (const entry of extensionFilesOrContents) { registerFileUrl(entry[0], EditorAppVscodeApi.verifyUrlorCreateDataUrl(entry[1])); @@ -41,7 +55,7 @@ export class EditorAppVscodeApi extends EditorAppBase { } } - await this.updateConfig(this.getAppConfig().userConfiguration ?? {}); + await this.updateEditorOptions(this.config.userConfiguration ?? {}); console.log('Init of VscodeApiConfig was completed.'); } @@ -49,7 +63,7 @@ export class EditorAppVscodeApi extends EditorAppBase { return (input instanceof URL) ? input.href : new URL(`data:text/plain;base64,${btoa(input)}`).href; } - async updateConfig(config: VscodeUserConfiguration) { + async updateEditorOptions(config: VscodeUserConfiguration) { if (config.json) { return updateUserConfiguration(config.json); } diff --git a/packages/monaco-editor-wrapper/src/index.ts b/packages/monaco-editor-wrapper/src/index.ts index c9a6689..5e9fafb 100644 --- a/packages/monaco-editor-wrapper/src/index.ts +++ b/packages/monaco-editor-wrapper/src/index.ts @@ -1,38 +1,45 @@ -export * from './editor.js'; - import { EditorAppBase, isVscodeApiEditorApp -} from './editor.js'; +} from './editorAppBase.js'; import type { - EditorAppConfig, + EditorAppBaseConfig, EditorAppType, VscodeUserConfiguration, -} from './editor.js'; +} from './editorAppBase.js'; import type { EditorAppConfigClassic, -} from './editorClassic.js'; +} from './editorAppClassic.js'; import { EditorAppClassic, -} from './editorClassic.js'; +} from './editorAppClassic.js'; import type { EditorAppConfigVscodeApi, -} from './editorVscodeApi.js'; +} from './editorAppVscodeApi.js'; import { EditorAppVscodeApi -} from './editorVscodeApi.js'; +} from './editorAppVscodeApi.js'; import type { + WebSocketCallOptions, + LanguageClientConfigType, WebSocketConfigOptions, WebSocketConfigOptionsUrl, WorkerConfigOptions, WorkerConfigDirect, LanguageClientConfig, +} from './languageClientWrapper.js'; + +import { + LanguageClientWrapper, +} from './languageClientWrapper.js'; + +import type { UserConfig, ModelUpdate, WrapperConfig @@ -44,11 +51,13 @@ import { export type { WrapperConfig, - EditorAppConfig, + EditorAppBaseConfig, EditorAppType, EditorAppConfigClassic, EditorAppConfigVscodeApi, VscodeUserConfiguration, + WebSocketCallOptions, + LanguageClientConfigType, WebSocketConfigOptions, WebSocketConfigOptionsUrl, WorkerConfigOptions, @@ -60,6 +69,7 @@ export type { export { MonacoEditorLanguageClientWrapper, + LanguageClientWrapper, EditorAppBase, isVscodeApiEditorApp, EditorAppClassic, diff --git a/packages/monaco-editor-wrapper/src/languageClientWrapper.ts b/packages/monaco-editor-wrapper/src/languageClientWrapper.ts new file mode 100644 index 0000000..5d13e26 --- /dev/null +++ b/packages/monaco-editor-wrapper/src/languageClientWrapper.ts @@ -0,0 +1,260 @@ +import { InitializeServiceConfig, initServices, MonacoLanguageClient, wasVscodeApiInitialized } from 'monaco-languageclient'; +import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'; +import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver-protocol/browser.js'; +import { CloseAction, ErrorAction, MessageTransports } from 'vscode-languageclient/lib/common/client.js'; +import { createUrl } from './utils.js'; + +export type WebSocketCallOptions = { + /** Adds handle on languageClient */ + onCall: () => void; + /** Reports Status Of Language Client */ + reportStatus?: boolean; +} + +export type LanguageClientConfigType = 'WebSocket' | 'WebSocketUrl' | 'WorkerConfig' | 'Worker'; + +export type WebSocketUrl = { + secured: boolean; + host: string; + port?: number; + path?: string; +} + +export type WebSocketConfigOptions = { + configType: 'WebSocket' + secured: boolean; + host: string; + port?: number; + path?: string; + startOptions?: WebSocketCallOptions; + stopOptions?: WebSocketCallOptions; +} + +export type WebSocketConfigOptionsUrl = { + configType: 'WebSocketUrl' + url: string; + startOptions?: WebSocketCallOptions; + stopOptions?: WebSocketCallOptions; +} + +export type WorkerConfigOptions = { + configType: 'WorkerConfig' + url: URL; + type: 'classic' | 'module'; + name?: string; +}; + +export type WorkerConfigDirect = { + configType: 'WorkerDirect'; + worker: Worker; +}; + +export type LanguageClientConfig = { + options: WebSocketConfigOptions | WebSocketConfigOptionsUrl | WorkerConfigOptions | WorkerConfigDirect; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initializationOptions?: any; +} + +export class LanguageClientWrapper { + + private serviceConfig: InitializeServiceConfig; + private languageClient: MonacoLanguageClient | undefined; + private languageClientConfig?: LanguageClientConfig; + private worker: Worker | undefined; + private languageId: string | undefined; + + constructor(languageClientConfig?: LanguageClientConfig, serviceConfig?: InitializeServiceConfig) { + if (languageClientConfig) { + this.languageClientConfig = languageClientConfig; + } + this.serviceConfig = serviceConfig ?? {}; + + // always set required services if not configure + this.serviceConfig.enableModelService = this.serviceConfig.enableModelService ?? true; + this.serviceConfig.configureEditorOrViewsServiceConfig = this.serviceConfig.configureEditorOrViewsServiceConfig ?? { + }; + this.serviceConfig.configureConfigurationServiceConfig = this.serviceConfig.configureConfigurationServiceConfig ?? { + defaultWorkspaceUri: '/tmp/' + }; + } + + haveLanguageClient(): boolean { + return this.languageClient !== undefined; + } + + haveLanguageClientConfig(): boolean { + return this.languageClientConfig !== undefined; + } + + getLanguageClient(): MonacoLanguageClient | undefined { + return this.languageClient; + } + + getWorker(): Worker | undefined { + return this.worker; + } + + isStarted(): boolean { + return this.languageClient !== undefined && this.languageClient?.isRunning(); + } + + async init(languageId: string) { + this.languageId = languageId; + await (wasVscodeApiInitialized() ? Promise.resolve('No service init on restart') : initServices(this.serviceConfig)); + } + + async start() { + if (this.languageClientConfig) { + console.log('Starting monaco-languageclient'); + await this.startLanguageClientConnection(); + } else { + await Promise.reject('Unable to start monaco-languageclient. No configuration was provided.'); + } + } + + /** + * Restart the languageclient with options to control worker handling + * + * @param updatedWorker Set a new worker here that should be used. keepWorker has no effect theb + * @param keepWorker Set to true if worker should not be disposed + */ + async restartLanguageClient(updatedWorker?: Worker, keepWorker?: boolean): Promise { + if (updatedWorker) { + await this.disposeLanguageClient(false); + } else { + await this.disposeLanguageClient(keepWorker); + } + this.worker = updatedWorker; + if (this.languageClientConfig) { + console.log('Re-Starting monaco-languageclient'); + await this.startLanguageClientConnection(); + } else { + await Promise.reject('Unable to restart languageclient. No configuration was provided.'); + } + } + + private startLanguageClientConnection(): Promise { + if (this.languageClient && this.languageClient.isRunning()) { + return Promise.resolve('monaco-languageclient already running!'); + } + + return new Promise((resolve, reject) => { + const lcConfig = this.languageClientConfig?.options; + if (lcConfig?.configType === 'WebSocket' || lcConfig?.configType === 'WebSocketUrl') { + const url = createUrl(lcConfig); + const webSocket = new WebSocket(url); + + webSocket.onopen = () => { + const socket = toSocket(webSocket); + const messageTransports = { + reader: new WebSocketMessageReader(socket), + writer: new WebSocketMessageWriter(socket) + }; + this.handleLanguageClientStart(messageTransports, resolve, reject); + }; + } else { + if (!this.worker) { + if (lcConfig?.configType === 'WorkerConfig') { + const workerConfig = lcConfig as WorkerConfigOptions; + this.worker = new Worker(new URL(workerConfig.url, window.location.href).href, { + type: workerConfig.type, + name: workerConfig.name + }); + } else { + const workerDirectConfig = lcConfig as WorkerConfigDirect; + this.worker = workerDirectConfig.worker; + } + } + const messageTransports = { + reader: new BrowserMessageReader(this.worker), + writer: new BrowserMessageWriter(this.worker) + }; + this.handleLanguageClientStart(messageTransports, resolve, reject); + } + }); + } + + private async handleLanguageClientStart(messageTransports: MessageTransports, + resolve: (value: string) => void, + reject: (reason?: unknown) => void) { + + this.languageClient = this.createLanguageClient(messageTransports); + const lcConfig = this.languageClientConfig?.options; + messageTransports.reader.onClose(async () => { + await this.languageClient?.stop(); + if ((lcConfig?.configType === 'WebSocket' || lcConfig?.configType === 'WebSocketUrl') && lcConfig?.stopOptions) { + const stopOptions = lcConfig?.stopOptions; + stopOptions.onCall(); + if (stopOptions.reportStatus) { + console.log(this.reportStatus().join('\n')); + } + } + }); + + try { + await this.languageClient.start(); + if ((lcConfig?.configType === 'WebSocket' || lcConfig?.configType === 'WebSocketUrl') && lcConfig?.startOptions) { + const startOptions = lcConfig?.startOptions; + startOptions.onCall(); + if (startOptions.reportStatus) { + console.log(this.reportStatus().join('\n')); + } + } + } catch (e) { + const errorMsg = `monaco-languageclient start was unsuccessful: ${e}`; + reject(errorMsg); + } + const msg = 'monaco-languageclient was successfully started.'; + resolve(msg); + } + + private createLanguageClient(transports: MessageTransports): MonacoLanguageClient { + return new MonacoLanguageClient({ + name: 'Monaco Wrapper Language Client', + clientOptions: { + // use a language id as a document selector + documentSelector: [this.languageId!], + // disable the default error handler + errorHandler: { + error: () => ({ action: ErrorAction.Continue }), + closed: () => ({ action: CloseAction.DoNotRestart }) + }, + // allow to initialize the language client with user specific options + initializationOptions: this.languageClientConfig?.initializationOptions + }, + // create a language client connection from the JSON RPC connection on demand + connectionProvider: { + get: () => { + return Promise.resolve(transports); + } + } + }); + } + + public async disposeLanguageClient(keepWorker?: boolean): Promise { + if (this.languageClient && this.languageClient.isRunning()) { + try { + await this.languageClient.dispose(); + if (keepWorker === undefined || keepWorker === false) { + this.worker?.terminate(); + this.worker = undefined; + } + this.languageClient = undefined; + await Promise.resolve('monaco-languageclient and monaco-editor were successfully disposed.'); + } catch (e) { + await Promise.reject(`Disposing the monaco-languageclient resulted in error: ${e}`); + } + } + else { + await Promise.reject('Unable to dispose monaco-languageclient: It is not yet started.'); + } + } + + reportStatus() { + const status: string[] = []; + status.push('LanguageClientWrapper status:'); + status.push(`LanguageClient: ${this.getLanguageClient()}`); + status.push(`Worker: ${this.getWorker()}`); + return status; + } +} diff --git a/packages/monaco-editor-wrapper/src/utils.ts b/packages/monaco-editor-wrapper/src/utils.ts index 2adfcf7..8604e12 100644 --- a/packages/monaco-editor-wrapper/src/utils.ts +++ b/packages/monaco-editor-wrapper/src/utils.ts @@ -1,4 +1,4 @@ -import { WebSocketConfigOptions, WebSocketConfigOptionsUrl } from './wrapper.js'; +import { WebSocketConfigOptions, WebSocketConfigOptionsUrl } from './languageClientWrapper.js'; export const createUrl = (config: WebSocketConfigOptions | WebSocketConfigOptionsUrl) => { let buildUrl = ''; diff --git a/packages/monaco-editor-wrapper/src/wrapper.ts b/packages/monaco-editor-wrapper/src/wrapper.ts index 64ec2d8..a66f83b 100644 --- a/packages/monaco-editor-wrapper/src/wrapper.ts +++ b/packages/monaco-editor-wrapper/src/wrapper.ts @@ -1,63 +1,9 @@ -import { EditorAppVscodeApi, EditorAppConfigVscodeApi } from './editorVscodeApi.js'; -import { EditorAppClassic, EditorAppConfigClassic } from './editorClassic.js'; +import { EditorAppVscodeApi, EditorAppConfigVscodeApi } from './editorAppVscodeApi.js'; +import { EditorAppClassic, EditorAppConfigClassic } from './editorAppClassic.js'; import { editor } from 'monaco-editor/esm/vs/editor/editor.api.js'; -import { InitializeServiceConfig, initServices, MonacoLanguageClient, wasVscodeApiInitialized } from 'monaco-languageclient'; -import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'; -import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver-protocol/browser.js'; -import { CloseAction, ErrorAction, MessageTransports } from 'vscode-languageclient/lib/common/client.js'; -import { createUrl } from './utils.js'; -import { VscodeUserConfiguration, isVscodeApiEditorApp } from './editor.js'; - -export type WebSocketCallOptions = { - /** Adds handle on languageClient */ - onCall: () => void; - /** Reports Status Of Language Client */ - reportStatus?: boolean; -} - -export type LanguageClientConfigType = 'WebSocket' | 'WebSocketUrl' | 'WorkerConfig' | 'Worker'; - -export type WebSocketUrl = { - secured: boolean; - host: string; - port?: number; - path?: string; -} - -export type WebSocketConfigOptions = { - configType: 'WebSocket' - secured: boolean; - host: string; - port?: number; - path?: string; - startOptions?: WebSocketCallOptions; - stopOptions?: WebSocketCallOptions; -} - -export type WebSocketConfigOptionsUrl = { - configType: 'WebSocketUrl' - url: string; - startOptions?: WebSocketCallOptions; - stopOptions?: WebSocketCallOptions; -} - -export type WorkerConfigOptions = { - configType: 'WorkerConfig' - url: URL; - type: 'classic' | 'module'; - name?: string; -}; - -export type WorkerConfigDirect = { - configType: 'WorkerDirect'; - worker: Worker; -}; - -export type LanguageClientConfig = { - options: WebSocketConfigOptions | WebSocketConfigOptionsUrl | WorkerConfigOptions | WorkerConfigDirect; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - initializationOptions?: any; -} +import { InitializeServiceConfig, MonacoLanguageClient } from 'monaco-languageclient'; +import { VscodeUserConfiguration, isVscodeApiEditorApp } from './editorAppBase.js'; +import { LanguageClientConfig, LanguageClientWrapper } from './languageClientWrapper.js'; export type WrapperConfig = { serviceConfig?: InitializeServiceConfig; @@ -79,17 +25,18 @@ export type ModelUpdate = { codeOriginalUri?: string; } +/** + * 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 { - private languageClient: MonacoLanguageClient | undefined; - private worker: Worker | undefined; - - private editorApp: EditorAppClassic | EditorAppVscodeApi | undefined; - private id: string; private htmlElement: HTMLElement; - private serviceConfig: InitializeServiceConfig; - private languageClientConfig?: LanguageClientConfig; + + private editorApp: EditorAppClassic | EditorAppVscodeApi | undefined; + private languageClientWrapper: LanguageClientWrapper; private init(userConfig: UserConfig) { if (userConfig.wrapperConfig.editorAppConfig.useDiffEditor && !userConfig.wrapperConfig.editorAppConfig.codeOriginal) { @@ -99,19 +46,7 @@ export class MonacoEditorLanguageClientWrapper { this.id = userConfig.id ?? Math.floor(Math.random() * 101).toString(); this.htmlElement = userConfig.htmlElement; - if (userConfig.languageClientConfig) { - this.languageClientConfig = userConfig.languageClientConfig; - } - - this.serviceConfig = userConfig.wrapperConfig.serviceConfig ?? {}; - - // always set required services if not configure - this.serviceConfig.enableModelService = this.serviceConfig.enableModelService ?? true; - this.serviceConfig.configureEditorOrViewsServiceConfig = this.serviceConfig.configureEditorOrViewsServiceConfig ?? { - }; - this.serviceConfig.configureConfigurationServiceConfig = this.serviceConfig.configureConfigurationServiceConfig ?? { - defaultWorkspaceUri: '/tmp/' - }; + this.languageClientWrapper = new LanguageClientWrapper(userConfig.languageClientConfig, userConfig.wrapperConfig.serviceConfig); } async start(userConfig: UserConfig) { @@ -126,15 +61,14 @@ export class MonacoEditorLanguageClientWrapper { } else { this.editorApp = new EditorAppClassic(this.id, userConfig); } - await (wasVscodeApiInitialized() ? Promise.resolve('No service init on restart') : initServices(this.serviceConfig)); + await this.languageClientWrapper.init(this.editorApp.getConfig().languageId); + console.log(`Starting monaco-editor (${this.id})`); + await this.editorApp?.init(); await this.editorApp.createEditors(this.htmlElement); - if (this.languageClientConfig) { - console.log('Starting monaco-languageclient'); - await this.startLanguageClientConnection(); - } else { - await Promise.resolve('All fine. monaco-languageclient is not used.'); + if (this.languageClientWrapper.haveLanguageClientConfig()) { + await this.languageClientWrapper.start(); } } @@ -144,8 +78,8 @@ export class MonacoEditorLanguageClientWrapper { return false; } - if (this.languageClientConfig) { - return this.languageClient !== undefined && this.languageClient.isRunning(); + if (this.languageClientWrapper.haveLanguageClient()) { + return this.languageClientWrapper.isStarted(); } return true; } @@ -163,7 +97,7 @@ export class MonacoEditorLanguageClientWrapper { } getLanguageClient(): MonacoLanguageClient | undefined { - return this.languageClient; + return this.languageClientWrapper.getLanguageClient(); } getModel(original?: boolean): editor.ITextModel | undefined { @@ -171,7 +105,7 @@ export class MonacoEditorLanguageClientWrapper { } getWorker(): Worker | undefined { - return this.worker; + return this.languageClientWrapper.getWorker(); } async updateModel(modelUpdate: ModelUpdate): Promise { @@ -184,40 +118,17 @@ export class MonacoEditorLanguageClientWrapper { async updateEditorOptions(options: editor.IEditorOptions & editor.IGlobalEditorOptions | VscodeUserConfiguration): Promise { if (this.editorApp) { - await this.editorApp.updateConfig(options); + await this.editorApp.updateEditorOptions(options); } else { await Promise.reject('Update was called when editor wrapper was not correctly configured.'); } } - /** - * Restart the languageclient with options to control worker handling - * - * @param updatedWorker Set a new worker here that should be used. keepWorker has no effect theb - * @param keepWorker Set to true if worker should not be disposed - */ - async restartLanguageClient(updatedWorker?: Worker, keepWorker?: boolean): Promise { - if (updatedWorker) { - await this.disposeLanguageClient(false); - } else { - await this.disposeLanguageClient(keepWorker); - } - this.worker = updatedWorker; - if (this.languageClientConfig) { - console.log('Re-Starting monaco-languageclient'); - await this.startLanguageClientConnection(); - } else { - await Promise.reject('Unable to restart languageclient. No configuration was provided.'); - } - } - public reportStatus() { const status: string[] = []; status.push('Wrapper status:'); status.push(`Editor: ${this.editorApp?.getEditor()}`); status.push(`DiffEditor: ${this.editorApp?.getDiffEditor()}`); - status.push(`LanguageClient: ${this.languageClient}`); - status.push(`Worker: ${this.worker}`); return status; } @@ -225,8 +136,8 @@ export class MonacoEditorLanguageClientWrapper { this.editorApp?.disposeEditor(); this.editorApp?.disposeDiffEditor(); - if (this.languageClientConfig) { - await this.disposeLanguageClient(false); + if (this.languageClientWrapper.haveLanguageClient()) { + await this.languageClientWrapper.disposeLanguageClient(false); this.editorApp = undefined; await Promise.resolve('Monaco editor and languageclient completed disposed.'); } @@ -235,125 +146,8 @@ export class MonacoEditorLanguageClientWrapper { } } - public async disposeLanguageClient(keepWorker?: boolean): Promise { - if (this.languageClient && this.languageClient.isRunning()) { - try { - await this.languageClient.dispose(); - if (keepWorker === undefined || keepWorker === false) { - this.worker?.terminate(); - this.worker = undefined; - } - this.languageClient = undefined; - await Promise.resolve('monaco-languageclient and monaco-editor were successfully disposed.'); - } catch (e) { - await Promise.reject(`Disposing the monaco-languageclient resulted in error: ${e}`); - } - } - else { - await Promise.reject('Unable to dispose monaco-languageclient: It is not yet started.'); - } - } - updateLayout() { this.editorApp?.updateLayout(); } - private startLanguageClientConnection(): Promise { - if (this.languageClient && this.languageClient.isRunning()) { - return Promise.resolve('monaco-languageclient already running!'); - } - - return new Promise((resolve, reject) => { - const lcConfig = this.languageClientConfig?.options; - if (lcConfig?.configType === 'WebSocket' || lcConfig?.configType === 'WebSocketUrl') { - const url = createUrl(lcConfig); - const webSocket = new WebSocket(url); - - webSocket.onopen = () => { - const socket = toSocket(webSocket); - const messageTransports = { - reader: new WebSocketMessageReader(socket), - writer: new WebSocketMessageWriter(socket) - }; - this.handleLanguageClientStart(messageTransports, resolve, reject); - }; - } else { - if (!this.worker) { - if (lcConfig?.configType === 'WorkerConfig') { - const workerConfig = lcConfig as WorkerConfigOptions; - this.worker = new Worker(new URL(workerConfig.url, window.location.href).href, { - type: workerConfig.type, - name: workerConfig.name - }); - } else { - const workerDirectConfig = lcConfig as WorkerConfigDirect; - this.worker = workerDirectConfig.worker; - } - } - const messageTransports = { - reader: new BrowserMessageReader(this.worker), - writer: new BrowserMessageWriter(this.worker) - }; - this.handleLanguageClientStart(messageTransports, resolve, reject); - } - }); - } - - private async handleLanguageClientStart(messageTransports: MessageTransports, - resolve: (value: string) => void, - reject: (reason?: unknown) => void) { - - this.languageClient = this.createLanguageClient(messageTransports); - const lcConfig = this.languageClientConfig?.options; - messageTransports.reader.onClose(async () => { - await this.languageClient?.stop(); - if ((lcConfig?.configType === 'WebSocket' || lcConfig?.configType === 'WebSocketUrl') && lcConfig?.stopOptions) { - const stopOptions = lcConfig?.stopOptions; - stopOptions.onCall(); - if (stopOptions.reportStatus) { - console.log(this.reportStatus().join('\n')); - } - } - }); - - try { - await this.languageClient.start(); - if ((lcConfig?.configType === 'WebSocket' || lcConfig?.configType === 'WebSocketUrl') && lcConfig?.startOptions) { - const startOptions = lcConfig?.startOptions; - startOptions.onCall(); - if (startOptions.reportStatus) { - console.log(this.reportStatus().join('\n')); - } - } - } catch (e) { - const errorMsg = `monaco-languageclient start was unsuccessful: ${e}`; - reject(errorMsg); - } - const msg = 'monaco-languageclient was successfully started.'; - resolve(msg); - } - - private createLanguageClient(transports: MessageTransports): MonacoLanguageClient { - return new MonacoLanguageClient({ - name: 'Monaco Wrapper Language Client', - clientOptions: { - // use a language id as a document selector - documentSelector: [this.editorApp!.getAppConfig().languageId], - // disable the default error handler - errorHandler: { - error: () => ({ action: ErrorAction.Continue }), - closed: () => ({ action: CloseAction.DoNotRestart }) - }, - // allow to initialize the language client with user specific options - initializationOptions: this.languageClientConfig?.initializationOptions - }, - // create a language client connection from the JSON RPC connection on demand - connectionProvider: { - get: () => { - return Promise.resolve(transports); - } - } - }); - } - } diff --git a/packages/monaco-editor-wrapper/test/editor.test.ts b/packages/monaco-editor-wrapper/test/editorAppBase.test.ts similarity index 100% rename from packages/monaco-editor-wrapper/test/editor.test.ts rename to packages/monaco-editor-wrapper/test/editorAppBase.test.ts diff --git a/packages/monaco-editor-wrapper/test/editorVscodeApi.test.ts b/packages/monaco-editor-wrapper/test/editorAppVscodeApi.test.ts similarity index 100% rename from packages/monaco-editor-wrapper/test/editorVscodeApi.test.ts rename to packages/monaco-editor-wrapper/test/editorAppVscodeApi.test.ts diff --git a/packages/monaco-editor-wrapper/test/languageClientWrapper.test.ts b/packages/monaco-editor-wrapper/test/languageClientWrapper.test.ts new file mode 100644 index 0000000..2a9c241 --- /dev/null +++ b/packages/monaco-editor-wrapper/test/languageClientWrapper.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from 'vitest'; +import { LanguageClientConfig, LanguageClientWrapper } from 'monaco-editor-wrapper'; + +describe('Test LanguageClientWrapper', () => { + + test('Not Running after construction', () => { + const languageClientWrapper = new LanguageClientWrapper(); + expect(languageClientWrapper.haveLanguageClient()).toBeFalsy(); + expect(languageClientWrapper.haveLanguageClientConfig()).toBeFalsy(); + expect(languageClientWrapper.isStarted()).toBeFalsy(); + }); + + test('Start: no config', async () => { + const languageClientWrapper = new LanguageClientWrapper(); + expect(async () => { + await languageClientWrapper.start(); + }).rejects.toEqual('Unable to start monaco-languageclient. No configuration was provided.'); + }); + + test('Start: config', async () => { + const languageClientConfig: LanguageClientConfig = { + options: { + configType: 'WebSocketUrl', + url: 'ws://localhost:3000/sampleServer' + } + }; + const languageClientWrapper = new LanguageClientWrapper(languageClientConfig); + expect(languageClientWrapper.haveLanguageClientConfig()).toBeTruthy(); + }); + +}); diff --git a/packages/monaco-editor-wrapper/test/wrapper.test.ts b/packages/monaco-editor-wrapper/test/wrapper.test.ts index a2cb72f..9fd4a33 100644 --- a/packages/monaco-editor-wrapper/test/wrapper.test.ts +++ b/packages/monaco-editor-wrapper/test/wrapper.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { EditorAppConfigClassic, MonacoEditorLanguageClientWrapper } from 'monaco-editor-wrapper'; +import { EditorAppClassic, MonacoEditorLanguageClientWrapper } from 'monaco-editor-wrapper'; import { buildWorkerDefinition } from 'monaco-editor-workers'; import { createBaseConfig, createMonacoEditorDiv } from './helper.js'; @@ -21,8 +21,13 @@ describe('Test MonacoEditorLanguageClientWrapper', () => { createMonacoEditorDiv(); const wrapper = new MonacoEditorLanguageClientWrapper(); await wrapper.start(createBaseConfig('classic')); - expect((wrapper.getMonacoEditorApp()?.getAppConfig() as EditorAppConfigClassic).automaticLayout).toBeTruthy(); - expect((wrapper.getMonacoEditorApp()?.getAppConfig() as EditorAppConfigClassic).theme).toBe('vs-light'); - expect(wrapper.getMonacoEditorApp()?.getAppType()).toBe('classic'); + + const app = wrapper.getMonacoEditorApp() as EditorAppClassic; + expect(app).toBeDefined(); + expect(app.getAppType()).toBe('classic'); + + const appConfig = app.getConfig(); + expect(appConfig.automaticLayout).toBeTruthy(); + expect(appConfig.theme).toBe('vs-light'); }); });