diff --git a/.gitignore b/.gitignore index 07e4af692..22a1810f5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ lib node_modules *.tsbuildinfo .angular +__screenshots__ diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 1f84cfe66..9a3bd7ed9 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -5,26 +5,22 @@ import { BaseLanguageClient, MessageTransports, LanguageClientOptions } from 'vscode-languageclient/browser.js'; -export interface IConnectionProvider { - get(encoding: string): Promise; -} - export type MonacoLanguageClientOptions = { name: string; id?: string; clientOptions: LanguageClientOptions; - connectionProvider: IConnectionProvider; + messageTransports: MessageTransports; } export class MonacoLanguageClient extends BaseLanguageClient { - protected readonly connectionProvider: IConnectionProvider; + protected readonly messageTransports: MessageTransports; - constructor({ id, name, clientOptions, connectionProvider }: MonacoLanguageClientOptions) { + constructor({ id, name, clientOptions, messageTransports }: MonacoLanguageClientOptions) { super(id ?? name.toLowerCase(), name, clientOptions); - this.connectionProvider = connectionProvider; + this.messageTransports = messageTransports; } - protected override createMessageTransports(encoding: string): Promise { - return this.connectionProvider.get(encoding); + protected override createMessageTransports(_encoding: string): Promise { + return Promise.resolve(this.messageTransports); } } diff --git a/packages/client/src/commonTypes.ts b/packages/client/src/commonTypes.ts index f37e15d9a..fe8a9ccba 100644 --- a/packages/client/src/commonTypes.ts +++ b/packages/client/src/commonTypes.ts @@ -20,7 +20,7 @@ export type LanguageClientRestartOptions = { export type LanguageClientConfigType = 'WebSocket' | 'WebSocketUrl' | 'WebSocketDirect' | 'WorkerConfig' | 'Worker'; -export type LanguageClientConfigOptions = (WebSocketConfigOptionsDirect | WebSocketConfigOptionsParams | WebSocketConfigOptionsUrl | WorkerConfigOptions | WorkerConfigDirect) & { +export type ConnetionConfigOptions = (WebSocketConfigOptionsDirect | WebSocketConfigOptionsParams | WebSocketConfigOptionsUrl | WorkerConfigOptions | WorkerConfigDirect) & { restartOptions?: LanguageClientRestartOptions; } diff --git a/packages/examples/src/bare/client.ts b/packages/examples/src/bare/client.ts index faf6f0fe1..8e6ca5e1e 100644 --- a/packages/examples/src/bare/client.ts +++ b/packages/examples/src/bare/client.ts @@ -73,7 +73,7 @@ export const initWebSocketAndStartClient = (url: string): WebSocket => { return webSocket; }; -export const createLanguageClient = (transports: MessageTransports): MonacoLanguageClient => { +export const createLanguageClient = (messageTransports: MessageTransports): MonacoLanguageClient => { return new MonacoLanguageClient({ name: 'Sample Language Client', clientOptions: { @@ -86,8 +86,6 @@ export const createLanguageClient = (transports: MessageTransports): MonacoLangu } }, // create a language client connection from the JSON RPC connection on demand - connectionProvider: { - get: async () => (transports) - } + messageTransports }); }; diff --git a/packages/examples/src/eclipse.jdt.ls/client/main.ts b/packages/examples/src/eclipse.jdt.ls/client/main.ts index 467d3b04e..adcd3cbd3 100644 --- a/packages/examples/src/eclipse.jdt.ls/client/main.ts +++ b/packages/examples/src/eclipse.jdt.ls/client/main.ts @@ -57,11 +57,13 @@ export const runEclipseJdtLsClient = () => { languageClientConfigs: { java: { languageId: 'java', - options: { - $type: 'WebSocketUrl', - url: 'ws://localhost:30003/jdtls' + connection: { + configOptions: { + $type: 'WebSocketUrl', + url: 'ws://localhost:30003/jdtls' + } }, - clientOptions: { + languageClientOptions: { documentSelector: ['java'], workspaceFolder: { index: 0, diff --git a/packages/examples/src/groovy/client/main.ts b/packages/examples/src/groovy/client/main.ts index 40489a2fe..97d873913 100644 --- a/packages/examples/src/groovy/client/main.ts +++ b/packages/examples/src/groovy/client/main.ts @@ -53,9 +53,11 @@ const userConfig: UserConfig = { languageClientConfigs: { groovy: { languageId: 'groovy', - options: { - $type: 'WebSocketUrl', - url: `ws://localhost:${groovyConfig.port}${groovyConfig.path}` + connection: { + configOptions: { + $type: 'WebSocketUrl', + url: `ws://localhost:${groovyConfig.port}${groovyConfig.path}` + } } } } diff --git a/packages/examples/src/json/client/wrapperWs.ts b/packages/examples/src/json/client/wrapperWs.ts index 2d8171aff..437b23b2b 100644 --- a/packages/examples/src/json/client/wrapperWs.ts +++ b/packages/examples/src/json/client/wrapperWs.ts @@ -54,20 +54,22 @@ export const jsonClientUserConfig: UserConfig = { languageClientConfigs: { json: { languageId: 'json', - options: { - $type: 'WebSocketUrl', - url: 'ws://localhost:30000/sampleServer', - startOptions: { - onCall: () => { - console.log('Connected to socket.'); + connection: { + configOptions: { + $type: 'WebSocketUrl', + url: 'ws://localhost:30000/sampleServer', + startOptions: { + onCall: () => { + console.log('Connected to socket.'); + }, + reportStatus: true }, - reportStatus: true - }, - stopOptions: { - onCall: () => { - console.log('Disconnected from socket.'); - }, - reportStatus: true + stopOptions: { + onCall: () => { + console.log('Disconnected from socket.'); + }, + reportStatus: true + } } } } diff --git a/packages/examples/src/langium/langium-dsl/config/classicConfig.ts b/packages/examples/src/langium/langium-dsl/config/classicConfig.ts index 629a2dda4..45119a349 100644 --- a/packages/examples/src/langium/langium-dsl/config/classicConfig.ts +++ b/packages/examples/src/langium/langium-dsl/config/classicConfig.ts @@ -52,9 +52,11 @@ export const setupLangiumClientClassic = async (): Promise => { languageClientConfigs: { langium: { languageId: 'langium', - options: { - $type: 'WorkerDirect', - worker: langiumWorker + connection: { + configOptions: { + $type: 'WorkerDirect', + worker: langiumWorker + } } } } diff --git a/packages/examples/src/langium/langium-dsl/config/extendedConfig.ts b/packages/examples/src/langium/langium-dsl/config/extendedConfig.ts index f9c66a6c4..d31252bc3 100644 --- a/packages/examples/src/langium/langium-dsl/config/extendedConfig.ts +++ b/packages/examples/src/langium/langium-dsl/config/extendedConfig.ts @@ -79,12 +79,12 @@ export const setupLangiumClientExtended = async (): Promise => { languageClientConfigs: { langium: { languageId: 'langium', - options: { - $type: 'WorkerDirect', - worker: langiumWorker - }, - connectionProvider: { - get: async () => ({ reader, writer }) + connection: { + configOptions: { + $type: 'WorkerDirect', + worker: langiumWorker + }, + messageTransports: { reader, writer } } } } diff --git a/packages/examples/src/langium/statemachine/config/wrapperStatemachineConfig.ts b/packages/examples/src/langium/statemachine/config/wrapperStatemachineConfig.ts index 1f660fd0c..6b383b980 100644 --- a/packages/examples/src/langium/statemachine/config/wrapperStatemachineConfig.ts +++ b/packages/examples/src/langium/statemachine/config/wrapperStatemachineConfig.ts @@ -6,12 +6,12 @@ import getKeybindingsServiceOverride from '@codingame/monaco-vscode-keybindings-service-override'; import getLifecycleServiceOverride from '@codingame/monaco-vscode-lifecycle-service-override'; import getLocalizationServiceOverride from '@codingame/monaco-vscode-localization-service-override'; -import { IConnectionProvider } from 'monaco-languageclient'; import { createDefaultLocaleConfiguration } from 'monaco-languageclient/vscode/services'; import { LanguageClientConfig, UserConfig } from 'monaco-editor-wrapper'; // cannot be imported with assert as json contains comments import statemachineLanguageConfig from './language-configuration.json?raw'; import responseStatemachineTm from '../syntaxes/statemachine.tmLanguage.json?raw'; +import { MessageTransports } from 'vscode-languageclient'; export const createLangiumGlobalConfig = async (params: { languageServerId: string, @@ -19,7 +19,7 @@ export const createLangiumGlobalConfig = async (params: { text?: string, worker?: Worker, messagePort?: MessagePort, - connectionProvider?: IConnectionProvider + messageTransports?: MessageTransports }): Promise => { const extensionFilesOrContents = new Map(); extensionFilesOrContents.set(`/${params.languageServerId}-statemachine-configuration.json`, statemachineLanguageConfig); @@ -36,12 +36,14 @@ export const createLangiumGlobalConfig = async (params: { const languageClientConfigs: Record | undefined = params.useLanguageClient && params.worker ? { statemachine: { languageId: 'statemachine', - options: { - $type: 'WorkerDirect', - worker: params.worker, - messagePort: params.messagePort, - }, - connectionProvider: params.connectionProvider + connection: { + configOptions: { + $type: 'WorkerDirect', + worker: params.worker, + messagePort: params.messagePort, + }, + messageTransports: params.messageTransports + } } } : undefined; diff --git a/packages/examples/src/langium/statemachine/main.ts b/packages/examples/src/langium/statemachine/main.ts index 48e5bc06c..a64819925 100644 --- a/packages/examples/src/langium/statemachine/main.ts +++ b/packages/examples/src/langium/statemachine/main.ts @@ -55,9 +55,7 @@ const startEditor = async () => { useLanguageClient: true, worker: stateMachineWorkerPort, messagePort: channel.port1, - connectionProvider: { - get: async () => ({ reader, writer }) - } + messageTransports: { reader, writer } }); await wrapper.initAndStart(langiumGlobalConfig, document.getElementById('monaco-editor-root')); diff --git a/packages/examples/src/python/client/config.ts b/packages/examples/src/python/client/config.ts index 5be57b2e8..96a183669 100644 --- a/packages/examples/src/python/client/config.ts +++ b/packages/examples/src/python/client/config.ts @@ -32,32 +32,32 @@ export const createUserConfig = (workspaceRoot: string, code: string, codeUri: s python: { languageId: 'python', name: 'Python Language Server Example', - options: { - $type: 'WebSocketDirect', - webSocket: webSocket, - startOptions: { - onCall: (languageClient?: MonacoLanguageClient) => { - setTimeout(() => { - ['pyright.restartserver', 'pyright.organizeimports'].forEach((cmdName) => { - vscode.commands.registerCommand(cmdName, (...args: unknown[]) => { - languageClient?.sendRequest('workspace/executeCommand', { command: cmdName, arguments: args }); + connection: { + configOptions: { + $type: 'WebSocketDirect', + webSocket: webSocket, + startOptions: { + onCall: (languageClient?: MonacoLanguageClient) => { + setTimeout(() => { + ['pyright.restartserver', 'pyright.organizeimports'].forEach((cmdName) => { + vscode.commands.registerCommand(cmdName, (...args: unknown[]) => { + languageClient?.sendRequest('workspace/executeCommand', { command: cmdName, arguments: args }); + }); }); - }); - }, 250); - }, - reportStatus: true, - } + }, 250); + }, + reportStatus: true, + } + }, + messageTransports: { reader, writer } }, - clientOptions: { + languageClientOptions: { documentSelector: ['python'], workspaceFolder: { index: 0, name: 'workspace', uri: vscode.Uri.parse(workspaceRoot) }, - }, - connectionProvider: { - get: async () => ({ reader, writer }) } } }, diff --git a/packages/examples/src/ts/wrapperAdvanced.ts b/packages/examples/src/ts/wrapperAdvanced.ts index 679ba6aa0..a2d356759 100644 --- a/packages/examples/src/ts/wrapperAdvanced.ts +++ b/packages/examples/src/ts/wrapperAdvanced.ts @@ -56,40 +56,44 @@ export const runMultipleLanguageClientsExample = async () => { json: { languageId: 'json', name: 'JSON Client', - options: { - $type: 'WebSocketParams', - host: 'localhost', - port: 30000, - path: 'sampleServer', - secured: false + connection: { + configOptions: { + $type: 'WebSocketParams', + host: 'localhost', + port: 30000, + path: 'sampleServer', + secured: false + } } }, python: { languageId: 'python', name: 'Python Client', - options: { - $type: 'WebSocketParams', - host: 'localhost', - port: 30001, - path: 'pyright', - secured: false, - extraParams: { - authorization: 'UserAuth' - }, - startOptions: { - onCall: (languageClient?: MonacoLanguageClient) => { - setTimeout(() => { - ['pyright.restartserver', 'pyright.organizeimports'].forEach((cmdName) => { - vscode.commands.registerCommand(cmdName, (...args: unknown[]) => { - languageClient?.sendRequest('workspace/executeCommand', { command: cmdName, arguments: args }); - }); - }); - }, 250); + connection: { + configOptions: { + $type: 'WebSocketParams', + host: 'localhost', + port: 30001, + path: 'pyright', + secured: false, + extraParams: { + authorization: 'UserAuth' }, - reportStatus: true, + startOptions: { + onCall: (languageClient?: MonacoLanguageClient) => { + setTimeout(() => { + ['pyright.restartserver', 'pyright.organizeimports'].forEach((cmdName) => { + vscode.commands.registerCommand(cmdName, (...args: unknown[]) => { + languageClient?.sendRequest('workspace/executeCommand', { command: cmdName, arguments: args }); + }); + }); + }, 250); + }, + reportStatus: true, + } } }, - clientOptions: { + languageClientOptions: { documentSelector: ['python'], workspaceFolder: { index: 0, diff --git a/packages/wrapper/src/index.ts b/packages/wrapper/src/index.ts index fcde29f30..091f5c51e 100644 --- a/packages/wrapper/src/index.ts +++ b/packages/wrapper/src/index.ts @@ -3,89 +3,21 @@ * Licensed under the MIT License. See LICENSE in the package root for license information. * ------------------------------------------------------------------------------------------ */ -import { - EditorAppBase, -} from './editorAppBase.js'; +export type * from './editorAppBase.js'; +export * from './editorAppBase.js'; -import type { - EditorAppConfigBase, - EditorAppType, - CodeContent, - CodePlusUri, - CodePlusFileExt, - CodeResources, - ModelRefs, - TextModels, - TextContents -} from './editorAppBase.js'; +export type * from './editorAppClassic.js'; +export * from './editorAppClassic.js'; -import type { - EditorAppConfigClassic, -} from './editorAppClassic.js'; +export type * from './editorAppExtended.js'; +export * from './editorAppExtended.js'; -import { - EditorAppClassic -} from './editorAppClassic.js'; +export type * from './languageClientWrapper.js'; +export * from './languageClientWrapper.js'; -import type { - ExtensionConfig, - EditorAppConfigExtended, - RegisterExtensionResult, - RegisterLocalProcessExtensionResult, - UserConfiguration -} from './editorAppExtended.js'; +export type * from './userConfig.js'; -import { - EditorAppExtended -} from './editorAppExtended.js'; - -import type { - LanguageClientConfig, - LanguageClientError -} from './languageClientWrapper.js'; - -import { - LanguageClientWrapper, -} from './languageClientWrapper.js'; - -import type { - UserConfig, - WrapperConfig -} from './userConfig.js'; - -import { - MonacoEditorLanguageClientWrapper, -} from './wrapper.js'; - -export type { - WrapperConfig, - EditorAppConfigBase, - EditorAppType, - EditorAppConfigClassic, - ExtensionConfig, - EditorAppConfigExtended, - RegisterExtensionResult, - RegisterLocalProcessExtensionResult, - UserConfiguration, - LanguageClientConfig, - LanguageClientError, - UserConfig, - CodeContent, - CodePlusUri, - CodePlusFileExt, - CodeResources, - ModelRefs, - TextModels, - TextContents -}; - -export { - MonacoEditorLanguageClientWrapper, - LanguageClientWrapper, - EditorAppBase, - EditorAppClassic, - EditorAppExtended -}; +export * from './wrapper.js'; export * from './utils.js'; export type * from './utils.js'; diff --git a/packages/wrapper/src/languageClientWrapper.ts b/packages/wrapper/src/languageClientWrapper.ts index b5acdcab2..e63677cc2 100644 --- a/packages/wrapper/src/languageClientWrapper.ts +++ b/packages/wrapper/src/languageClientWrapper.ts @@ -3,19 +3,23 @@ * Licensed under the MIT License. See LICENSE in the package root for license information. * ------------------------------------------------------------------------------------------ */ -import { MonacoLanguageClient, IConnectionProvider, WorkerConfigOptions, WorkerConfigDirect, LanguageClientConfigOptions, WebSocketConfigOptionsParams, WebSocketConfigOptionsDirect, WebSocketConfigOptionsUrl, LanguageClientRestartOptions } from 'monaco-languageclient'; +import { MonacoLanguageClient, WorkerConfigOptions, WorkerConfigDirect, LanguageClientRestartOptions, ConnetionConfigOptions } from 'monaco-languageclient'; import { Logger } from 'monaco-languageclient/tools'; import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver-protocol/browser.js'; import { CloseAction, ErrorAction, LanguageClientOptions, MessageTransports, State } from 'vscode-languageclient/browser.js'; import { createUrl } from './utils.js'; import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'; +export type ConnectionConfig = { + configOptions: ConnetionConfigOptions; + messageTransports?: MessageTransports; +} + export type LanguageClientConfig = { - languageId: string; - options: LanguageClientConfigOptions; name?: string; - clientOptions?: LanguageClientOptions; - connectionProvider?: IConnectionProvider; + languageId: string; + connection: ConnectionConfig; + languageClientOptions?: LanguageClientOptions; } export type LanguageClientError = { @@ -26,14 +30,14 @@ export type LanguageClientError = { export class LanguageClientWrapper { private languageClient?: MonacoLanguageClient; - private languageClientConfig?: LanguageClientConfig; + private languageClientConfig: LanguageClientConfig; private languageId: string; private worker?: Worker; private port?: MessagePort; private name?: string; private logger: Logger | undefined; - async init(config: { + constructor(config: { languageClientConfig: LanguageClientConfig, logger?: Logger }) { @@ -47,10 +51,6 @@ export class LanguageClientWrapper { return this.languageClient !== undefined; } - haveLanguageClientConfig(): boolean { - return this.languageClientConfig !== undefined; - } - getLanguageClient(): MonacoLanguageClient | undefined { return this.languageClient; } @@ -63,16 +63,25 @@ export class LanguageClientWrapper { return this.languageClient !== undefined && this.languageClient.isRunning(); } - async start() { - if (this.languageClientConfig) { - return this.startLanguageClientConnection(); - } else { - const languageClientError: LanguageClientError = { - message: `languageClientWrapper (${this.name}): Unable to start monaco-languageclient. No configuration was provided.`, - error: 'No error was provided.' - }; - return Promise.reject(languageClientError); + async start(): Promise { + if (this.languageClient?.isRunning() ?? false) { + this.logger?.info('startLanguageClientConnection: monaco-languageclient already running!'); + return Promise.resolve(); } + + // eslint-disable-next-line no-async-promise-executor + return new Promise((resolve, reject) => { + const conConfig = this.languageClientConfig.connection; + const conOptions = conConfig.configOptions; + + if (conOptions.$type === 'WebSocketDirect' || conOptions.$type === 'WebSocketParams' || conOptions.$type === 'WebSocketUrl') { + const webSocket = conOptions.$type === 'WebSocketDirect' ? conOptions.webSocket : new WebSocket(createUrl(conOptions)); + this.initMessageTransportWebSocket(webSocket, resolve, reject); + } else { + // init of worker and start of languageclient can be handled directly, because worker available already + this.initMessageTransportWorker(conOptions, resolve, reject); + } + }); } /** @@ -85,60 +94,29 @@ export class LanguageClientWrapper { await this.disposeLanguageClient(keepWorker); this.worker = updatedWorker; - if (this.languageClientConfig) { - this.logger?.info('Re-Starting monaco-languageclient'); - return this.startLanguageClientConnection(); - } else { - const languageClientError: LanguageClientError = { - message: `languageClientWrapper (${this.name}): Unable to restart languageclient. No configuration was provided.`, - error: 'No error was provided.' - }; - return Promise.reject(languageClientError); - } + this.logger?.info('Re-Starting monaco-languageclient'); + return this.start(); } - protected async startLanguageClientConnection(): Promise { - if (this.languageClient?.isRunning() ?? false) { - this.logger?.info('startLanguageClientConnection: monaco-languageclient already running!'); - return Promise.resolve(); - } - const lccOptions = this.languageClientConfig?.options; - - // eslint-disable-next-line no-async-promise-executor - return new Promise((resolve, reject) => { - if (lccOptions === undefined) { - reject('Unable to start languageclient, because configuration options are not provided.'); - } else { - if (lccOptions.$type === 'WebSocketDirect' || lccOptions.$type === 'WebSocketParams' || lccOptions.$type === 'WebSocketUrl') { - this.initMessageTransportWebSocket(lccOptions, resolve, reject); - } else { - // init of worker and start of languageclient can be handled directly, because worker available already - this.initMessageTransportWorker(lccOptions, resolve, reject); - } - } - }); - } - - protected async initMessageTransportWebSocket(lccOptions: WebSocketConfigOptionsDirect | WebSocketConfigOptionsParams | WebSocketConfigOptionsUrl, - resolve: () => void, reject: (reason?: unknown) => void) { - const webSocket = lccOptions.$type === 'WebSocketDirect' ? lccOptions.webSocket : new WebSocket(createUrl(lccOptions)); + protected async initMessageTransportWebSocket(webSocket: WebSocket, resolve: () => void, reject: (reason?: unknown) => void) { - const createMessageTransports = (transport: Worker | MessagePort | WebSocket) => { - const iWebSocket = toSocket(transport as WebSocket); - return { + let messageTransports = this.languageClientConfig.connection.messageTransports; + if (messageTransports === undefined) { + const iWebSocket = toSocket(webSocket); + messageTransports = { reader: new WebSocketMessageReader(iWebSocket), writer: new WebSocketMessageWriter(iWebSocket) }; - }; + } // if websocket is already open, then start the languageclient directly if (webSocket.readyState === WebSocket.OPEN) { - await this.performLanguageClientStart(webSocket, createMessageTransports, resolve, reject); + await this.performLanguageClientStart(messageTransports, resolve, reject); } // otherwise start on open webSocket.onopen = async () => { - await this.performLanguageClientStart(webSocket, createMessageTransports, resolve, reject); + await this.performLanguageClientStart(messageTransports, resolve, reject); }; webSocket.onerror = (ev: Event) => { const languageClientError: LanguageClientError = { @@ -175,28 +153,28 @@ export class LanguageClientWrapper { } const portOrWorker = this.port ? this.port : this.worker; - const createMessageTransports = (transport: Worker | MessagePort | WebSocket) => { - return { - reader: new BrowserMessageReader(transport), - writer: new BrowserMessageWriter(transport) + let messageTransports = this.languageClientConfig.connection.messageTransports; + if (messageTransports === undefined) { + messageTransports = { + reader: new BrowserMessageReader(portOrWorker), + writer: new BrowserMessageWriter(portOrWorker) }; - }; - await this.performLanguageClientStart(portOrWorker, createMessageTransports, resolve, reject); + } + + await this.performLanguageClientStart(messageTransports, resolve, reject); } - protected async performLanguageClientStart(transport: Worker | MessagePort | WebSocket, - createMessageTransports: (transport: Worker | MessagePort | WebSocket) => MessageTransports, - resolve: () => void, reject: (reason?: unknown) => void) { + protected async performLanguageClientStart(messageTransports: MessageTransports, resolve: () => void, reject: (reason?: unknown) => void) { // do not perform another start attempt if already running if (this.languageClient?.isRunning() ?? false) { this.logger?.info('performLanguageClientStart: monaco-languageclient already running!'); resolve(); } const mlcConfig = { - name: this.languageClientConfig?.name ?? 'Monaco Wrapper Language Client', + name: this.languageClientConfig.name ?? 'Monaco Wrapper Language Client', // allow to fully override the clientOptions - clientOptions: this.languageClientConfig?.clientOptions ?? { + clientOptions: this.languageClientConfig.languageClientOptions ?? { documentSelector: [this.languageId], // disable the default error handler errorHandler: { @@ -204,22 +182,19 @@ export class LanguageClientWrapper { closed: () => ({ action: CloseAction.DoNotRestart }) } }, - connectionProvider: this.languageClientConfig?.connectionProvider ?? { - get: async () => createMessageTransports(transport) - } + messageTransports }; this.languageClient = new MonacoLanguageClient(mlcConfig); - const messageTransports = await mlcConfig.connectionProvider.get('utf-8'); - const lccOptions = this.languageClientConfig?.options; - this.initRestartConfiguration(messageTransports, lccOptions?.restartOptions); + const conOptions = this.languageClientConfig.connection.configOptions; + this.initRestartConfiguration(messageTransports, conOptions.restartOptions); messageTransports.reader.onClose(async () => { await this.languageClient?.stop(); - if ((lccOptions?.$type === 'WebSocketParams' || lccOptions?.$type === 'WebSocketUrl') && lccOptions.stopOptions) { - const stopOptions = lccOptions.stopOptions; + if ((conOptions.$type === 'WebSocketParams' || conOptions.$type === 'WebSocketUrl') && conOptions.stopOptions !== undefined) { + const stopOptions = conOptions.stopOptions; stopOptions.onCall(this.getLanguageClient()); if (stopOptions.reportStatus !== undefined) { this.logger?.info(this.reportStatus().join('\n')); @@ -230,8 +205,8 @@ export class LanguageClientWrapper { try { await this.languageClient.start(); - if ((lccOptions?.$type === 'WebSocketParams' || lccOptions?.$type === 'WebSocketUrl') && lccOptions.startOptions) { - const startOptions = lccOptions.startOptions; + if ((conOptions.$type === 'WebSocketParams' || conOptions.$type === 'WebSocketUrl') && conOptions.startOptions !== undefined) { + const startOptions = conOptions.startOptions; startOptions.onCall(this.getLanguageClient()); if (startOptions.reportStatus !== undefined) { this.logger?.info(this.reportStatus().join('\n')); @@ -275,10 +250,6 @@ export class LanguageClientWrapper { } }; } - const successCallback = messageTransports.reader.listen(() => { - this.logger?.info('MessageTransport Reader started listening.'); - successCallback.dispose(); - }); } protected disposeWorker(keepWorker?: boolean) { diff --git a/packages/wrapper/src/workerFactory.ts b/packages/wrapper/src/workerFactory.ts index e4db1c1a0..b9f3f50d3 100644 --- a/packages/wrapper/src/workerFactory.ts +++ b/packages/wrapper/src/workerFactory.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See LICENSE in the package root for license information. * ------------------------------------------------------------------------------------------ */ +import { Logger } from 'monaco-languageclient/tools'; import { initEnhancedMonacoEnvironment } from 'monaco-languageclient/vscode/services'; export type WorkerOverrides = { @@ -60,7 +61,7 @@ export const defaultWorkerLoaders: Partial { +export const buildWorker = (config: WorkerConfig, workerOverrides?: WorkerOverrides, logger?: Logger): Worker => { if (workerOverrides?.rootPath !== undefined) { config.rootPath = workerOverrides.rootPath; } @@ -72,7 +73,7 @@ export const buildWorker = (config: WorkerConfig, workerOverrides?: WorkerOverri workerFile = `${config.basePath}/${config.workerFile}`; } const fullUrl = new URL(workerFile, config.rootPath).href; - console.log(`Creating worker: ${fullUrl}`); + logger?.info(`Creating worker: ${fullUrl}`); // default to 'module' if not specified const workerOptions = config.options ?? {}; @@ -85,11 +86,11 @@ export const buildWorker = (config: WorkerConfig, workerOverrides?: WorkerOverri return new Worker(URL.createObjectURL(blob), workerOptions); }; -export const useWorkerFactory = (workerOverrides?: WorkerOverrides) => { +export const useWorkerFactory = (workerOverrides?: WorkerOverrides, logger?: Logger) => { const envEnhanced = initEnhancedMonacoEnvironment(); const getWorker = (moduleId: string, label: string) => { - console.log(`getWorker: moduleId: ${moduleId} label: ${label}`); + logger?.info(`getWorker: moduleId: ${moduleId} label: ${label}`); let selector = label; let workerLoaders; @@ -116,7 +117,7 @@ export const useWorkerFactory = (workerOverrides?: WorkerOverrides) => { if (workerOrConfig) { const invoked = workerOrConfig(); if (Object.hasOwn(invoked, 'workerFile')) { - return buildWorker(invoked as WorkerConfig, workerOverrides); + return buildWorker(invoked as WorkerConfig, workerOverrides, logger); } else { return invoked as Worker; } diff --git a/packages/wrapper/src/wrapper.ts b/packages/wrapper/src/wrapper.ts index 07f3bb592..fb00f14f8 100644 --- a/packages/wrapper/src/wrapper.ts +++ b/packages/wrapper/src/wrapper.ts @@ -74,13 +74,11 @@ export class MonacoEditorLanguageClientWrapper { if (lccs !== undefined && Object.entries(lccs).length > 0) { for (const [languageId, lcc] of Object.entries(lccs)) { - const lcw = new LanguageClientWrapper(); - this.languageClientWrappers.set(languageId, lcw); - - lcw.init({ + const lcw = new LanguageClientWrapper({ languageClientConfig: lcc, logger: this.logger }); + this.languageClientWrappers.set(languageId, lcw); } } @@ -111,9 +109,7 @@ export class MonacoEditorLanguageClientWrapper { await this.editorApp?.createEditors(htmlElement); for (const lcw of this.languageClientWrappers.values()) { - if (lcw.haveLanguageClientConfig()) { - await lcw.start(); - } + await lcw.start(); } this.markStarted(); diff --git a/packages/wrapper/test/helper.ts b/packages/wrapper/test/helper.ts index 098dcb01d..5887c410d 100644 --- a/packages/wrapper/test/helper.ts +++ b/packages/wrapper/test/helper.ts @@ -35,12 +35,3 @@ export const createEditorAppConfig = (type: EditorAppType) => { useDiffEditor: false, }; }; - -/** - * Helper to generate a quick worker from a function blob - */ -export const createWorkerFromFunction = (fn: () => void): Worker => { - return new Worker(URL.createObjectURL( - new Blob([`(${fn.toString()})()`], { type: 'application/javascript' }) - )); -}; diff --git a/packages/wrapper/test/languageClientWrapper.test.ts b/packages/wrapper/test/languageClientWrapper.test.ts index 387b8145f..0fe55f52e 100644 --- a/packages/wrapper/test/languageClientWrapper.test.ts +++ b/packages/wrapper/test/languageClientWrapper.test.ts @@ -5,7 +5,7 @@ import { describe, expect, test } from 'vitest'; import { LanguageClientConfig, LanguageClientWrapper, MonacoEditorLanguageClientWrapper } from 'monaco-editor-wrapper'; -import { createBaseConfig, createWorkerFromFunction } from './helper.js'; +import { createBaseConfig } from './helper.js'; describe('Test LanguageClientWrapper', () => { @@ -19,30 +19,32 @@ describe('Test LanguageClientWrapper', () => { test('Constructor: no config', async () => { // create a web worker to pass to the wrapper - const worker = createWorkerFromFunction(() => { - console.info('Hello'); + const worker = new Worker('./worker/langium-server.ts', { + type: 'module', + name: 'Langium LS', }); const languageClientConfig: LanguageClientConfig = { languageId: 'javascript', - options: { - $type: 'WorkerDirect', - worker + connection: { + configOptions: { + $type: 'WorkerDirect', + worker + } } }; - const languageClientWrapper = new LanguageClientWrapper(); - languageClientWrapper.init({ + const languageClientWrapper = new LanguageClientWrapper({ languageClientConfig }); expect(languageClientWrapper).toBeDefined(); expect(languageClientWrapper.haveLanguageClient).toBeTruthy(); - expect(languageClientWrapper.haveLanguageClientConfig).toBeTruthy(); }); test('Dispose: direct worker is cleaned up afterwards', async () => { // create a web worker to pass to the wrapper - const worker = createWorkerFromFunction(() => { - console.info('Hello'); + const worker = new Worker('./worker/langium-server.ts', { + type: 'module', + name: 'Langium LS', }); // setup the wrapper @@ -50,9 +52,11 @@ describe('Test LanguageClientWrapper', () => { config.languageClientConfigs = { javascript: { languageId: 'javascript', - options: { - $type: 'WorkerDirect', - worker + connection: { + configOptions: { + $type: 'WorkerDirect', + worker + } } } }; @@ -62,13 +66,18 @@ describe('Test LanguageClientWrapper', () => { const languageClientWrapper = wrapper.getLanguageClientWrapper('javascript'); expect(languageClientWrapper).toBeDefined(); - // start up & verify (don't wait for start to finish, just roll past it, we only care about the worker) - languageClientWrapper!.start(); - expect(languageClientWrapper!.getWorker()).toBeTruthy(); + expect(languageClientWrapper?.getWorker()).toBeFalsy(); + + // WA: for whatever reasons "await" kills the test, + // but the languageClientWrapper needs to be fully initialised as otherwise the follow up steps fail + languageClientWrapper?.start(); - // dispose & verify - languageClientWrapper!.disposeLanguageClient(); - expect(languageClientWrapper!.getWorker()).toBeUndefined(); + setTimeout(async () => { + // dispose & verify + await languageClientWrapper?.disposeLanguageClient(); + expect(languageClientWrapper?.getWorker()).toBeUndefined(); + }, 250); + expect(languageClientWrapper?.getWorker()).toBeTruthy(); }); test('Constructor: config', async () => { @@ -76,9 +85,11 @@ describe('Test LanguageClientWrapper', () => { config.languageClientConfigs = { javascript: { languageId: 'javascript', - options: { - $type: 'WebSocketUrl', - url: 'ws://localhost:12345/Tester' + connection: { + configOptions: { + $type: 'WebSocketUrl', + url: 'ws://localhost:12345/Tester' + } } } }; @@ -87,8 +98,6 @@ describe('Test LanguageClientWrapper', () => { const languageClientWrapper = wrapper.getLanguageClientWrapper('javascript'); expect(languageClientWrapper).toBeDefined(); - - expect(languageClientWrapper!.haveLanguageClientConfig()).toBeTruthy(); }); test('Start: unreachable url', async () => { @@ -97,9 +106,11 @@ describe('Test LanguageClientWrapper', () => { javascript: { languageId: 'javascript', name: 'test-unreachable', - options: { - $type: 'WebSocketUrl', - url: 'ws://localhost:12345/Tester' + connection: { + configOptions: { + $type: 'WebSocketUrl', + url: 'ws://localhost:12345/Tester' + } } } }; @@ -109,8 +120,6 @@ describe('Test LanguageClientWrapper', () => { const languageClientWrapper = wrapper.getLanguageClientWrapper('javascript'); expect(languageClientWrapper).toBeDefined(); - expect(languageClientWrapper!.haveLanguageClientConfig()).toBeTruthy(); - console.log('start'); await expect(languageClientWrapper!.start()).rejects.toEqual({ message: 'languageClientWrapper (test-unreachable): Websocket connection failed.', error: 'No error was provided.' @@ -133,10 +142,12 @@ describe('Test LanguageClientWrapper', () => { config.languageClientConfigs = { javascript: { languageId: 'javascript', - options: { - $type: 'WorkerConfig', - url: new URL('http://localhost:20101'), - type: 'classic' + connection: { + configOptions: { + $type: 'WorkerConfig', + url: new URL('http://localhost:20101'), + type: 'classic' + } } } }; @@ -146,7 +157,6 @@ describe('Test LanguageClientWrapper', () => { const languageClientWrapper = wrapper.getLanguageClientWrapper('javascript'); expect(languageClientWrapper).toBeDefined(); - expect(languageClientWrapper!.haveLanguageClientConfig()).toBeTruthy(); await expect(languageClientWrapper!.start()).rejects.toEqual({ message: 'languageClientWrapper (unnamed): Illegal worker configuration detected. Potentially the url is wrong.', error: 'No error was provided.' diff --git a/packages/wrapper/test/worker/langium-server.ts b/packages/wrapper/test/worker/langium-server.ts new file mode 100644 index 000000000..be1c511f5 --- /dev/null +++ b/packages/wrapper/test/worker/langium-server.ts @@ -0,0 +1,24 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2018-2022 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ +/// + +import { EmptyFileSystem } from 'langium'; +import { DefaultSharedModuleContext, startLanguageServer } from 'langium/lsp'; +import { createLangiumGrammarServices } from 'langium/grammar'; +import { BrowserMessageReader, BrowserMessageWriter, createConnection } from 'vscode-languageserver/browser.js'; + +/* browser specific setup code */ +const messageReader = new BrowserMessageReader(self as DedicatedWorkerGlobalScope); +const messageWriter = new BrowserMessageWriter(self as DedicatedWorkerGlobalScope); + +// Inject the shared services and language-specific services +const context = { + connection: createConnection(messageReader, messageWriter), + ...EmptyFileSystem +} as unknown as DefaultSharedModuleContext; +const { shared } = createLangiumGrammarServices(context); + +// Start the language server with the shared services +startLanguageServer(shared);