/******************************************************************************** * Copyright (C) 2017 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * This Source Code may also be made available under the following Secondary * Licenses when the conditions for such availability set forth in the Eclipse * Public License v. 2.0 are satisfied: GNU General Public License, version 2 * with the GNU Classpath Exception which is available at * https://www.gnu.org/software/classpath/license.html. * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ /* eslint-disable @typescript-eslint/no-explicit-any */ import { injectable, inject } from 'inversify'; import { MaybePromise, MessageService, CommandRegistry } from '@theia/core'; import { Disposable, DisposableCollection } from '@theia/core/lib/common'; import { FrontendApplication, WebSocketConnectionProvider } from '@theia/core/lib/browser'; import { LanguageContribution, ILanguageClient, LanguageClientOptions, DocumentSelector, TextDocument, FileSystemWatcher, Workspace, Languages, State } from './language-client-services'; import { MessageConnection, ResponseError } from 'vscode-jsonrpc'; import { LanguageClientFactory } from './language-client-factory'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { InitializeParams } from 'monaco-languageclient'; import { Deferred } from '@theia/core/lib/common/promise-util'; /** * @deprecated since 1.4.0 - in order to remove monaco-languageclient, use VS Code extensions to contribute language smartness: * https://code.visualstudio.com/api/language-extensions/language-server-extension-guide */ export const LanguageClientContribution = Symbol('LanguageClientContribution'); export interface LanguageClientContribution extends LanguageContribution { readonly running: boolean; readonly languageClient: Promise; waitForActivation(app: FrontendApplication): Promise; activate(app: FrontendApplication): Disposable; deactivate(): void; restart(): void; } /** * @deprecated since 1.4.0 - in order to remove monaco-languageclient, use VS Code extensions to contribute language smartness: * https://code.visualstudio.com/api/language-extensions/language-server-extension-guide */ @injectable() export abstract class BaseLanguageClientContribution implements LanguageClientContribution { abstract readonly id: string; abstract readonly name: string; protected _languageClient: ILanguageClient | undefined; protected resolveReady: (languageClient: ILanguageClient) => void; protected ready: Promise; @inject(MessageService) protected readonly messageService: MessageService; @inject(CommandRegistry) protected readonly registry: CommandRegistry; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(LanguageContribution.Service) protected readonly languageContributionService: LanguageContribution.Service; @inject(WebSocketConnectionProvider) protected readonly connectionProvider: WebSocketConnectionProvider; constructor( @inject(Workspace) protected readonly workspace: Workspace, @inject(Languages) protected readonly languages: Languages, @inject(LanguageClientFactory) protected readonly languageClientFactory: LanguageClientFactory ) { this.waitForReady(); } get languageClient(): Promise { return this._languageClient ? Promise.resolve(this._languageClient) : this.ready; } // eslint-disable-next-line @typescript-eslint/no-explicit-any waitForActivation(app: FrontendApplication): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const activationPromises: Promise[] = []; const workspaceContains = this.workspaceContains; if (workspaceContains.length !== 0) { activationPromises.push(this.waitForItemInWorkspace()); } const documentSelector = this.documentSelector; if (documentSelector) { activationPromises.push(this.waitForOpenTextDocument(documentSelector)); } if (activationPromises.length !== 0) { return Promise.all([ this.workspace.ready, Promise.race(activationPromises.map(p => new Promise(async resolve => { try { await p; resolve(); } catch (e) { console.error(e); } }))) ]); } return this.workspace.ready; } protected deferredConnection = new Deferred(); protected readonly toDeactivate = new DisposableCollection(); activate(): Disposable { if (this.toDeactivate.disposed) { if (!this._languageClient) { this._languageClient = this.createLanguageClient(() => this.deferredConnection.promise); this._languageClient.onDidChangeState(({ newState }) => { this.state = newState; }); } const toStop = new DisposableCollection(Disposable.create(() => { })); // mark as not disposed this.toDeactivate.push(toStop); this.doActivate(toStop); } return this.toDeactivate; } deactivate(): void { this.toDeactivate.dispose(); } protected stop = Promise.resolve(); protected async doActivate(toStop: DisposableCollection): Promise { try { // make sure that the previous client is stopped to avoid duplicate commands and language services await this.stop; if (toStop.disposed) { return; } const startParameters = await this.getStartParameters(); if (toStop.disposed) { return; } const sessionId = await this.languageContributionService.create(this.id, startParameters); if (toStop.disposed) { this.languageContributionService.destroy(sessionId); return; } toStop.push(Disposable.create(() => this.languageContributionService.destroy(sessionId))); this.connectionProvider.listen({ path: LanguageContribution.getPath(this, sessionId), onConnection: messageConnection => { this.deferredConnection.resolve(messageConnection); messageConnection.onDispose(() => this.deferredConnection = new Deferred()); if (toStop.disposed) { messageConnection.dispose(); return; } toStop.push(Disposable.create(() => this.stop = (async () => { try { // avoid calling stop if start failed await this._languageClient!.onReady(); // remove all listeners await this._languageClient!.stop(); } catch { try { // if start or stop failed make sure the the connection is closed messageConnection.dispose(); } catch { /* no-op */ } } })())); toStop.push(messageConnection.onClose(() => this.forceRestart())); this._languageClient!.start(); // it should be called after `start` that `onReady` promise gets reinitialized this.onWillStart(this._languageClient!, toStop); } }, { reconnecting: false }); } catch (e) { console.error(e); if (!toStop.disposed) { this.forceRestart(); } } } protected state: State | undefined; get running(): boolean { return !this.toDeactivate.disposed && this.state === State.Running; } restart(): void { if (!this.running) { return; } this.forceRestart(); } protected forceRestart(): void { this.deactivate(); this.activate(); } protected onWillStart(languageClient: ILanguageClient, toStop?: DisposableCollection): void { languageClient.onReady().then(() => this.onReady(languageClient, toStop)); } protected onReady(languageClient: ILanguageClient, toStop?: DisposableCollection): void { this._languageClient = languageClient; this.resolveReady(this._languageClient); this.waitForReady(); } protected waitForReady(): void { this.ready = new Promise(resolve => this.resolveReady = resolve ); } protected createLanguageClient(connection: MessageConnection | (() => MaybePromise)): ILanguageClient { const clientOptions = this.createOptions(); return this.languageClientFactory.get(this, clientOptions, connection); } protected createOptions(): LanguageClientOptions { const { id, documentSelector, fileEvents, configurationSection, initializationOptions } = this; return { documentSelector, synchronize: { fileEvents, configurationSection }, initializationFailedHandler: err => this.handleInitializationFailed(err), diagnosticCollectionName: id, initializationOptions }; } protected handleInitializationFailed(err: ResponseError | Error | any): boolean { this.deactivate(); const detail = err instanceof Error ? `: ${err.message}` : '.'; this.messageService.error(`Failed to start ${this.name} language server${detail}`, 'Retry').then(result => { if (result) { this.activate(); } }); return false; } protected getStartParameters(): MaybePromise { return undefined; } // eslint-disable-next-line @typescript-eslint/no-explicit-any protected get initializationOptions(): any | (() => any) | undefined { return undefined; } protected get configurationSection(): string | string[] | undefined { return undefined; } protected get workspaceContains(): string[] { return []; } protected get documentSelector(): DocumentSelector | undefined { return [this.id]; } protected _fileEvents: FileSystemWatcher[] | undefined; protected get fileEvents(): FileSystemWatcher[] { return this._fileEvents = this._fileEvents || this.createFileEvents(); } protected createFileEvents(): FileSystemWatcher[] { const watchers = []; if (this.workspace.createFileSystemWatcher) { for (const globPattern of this.globPatterns) { watchers.push(this.workspace.createFileSystemWatcher(globPattern)); } } return watchers; } protected get globPatterns(): string[] { return []; } /** * Check to see if one of the paths is in the current workspace. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any protected async waitForItemInWorkspace(): Promise { const doesContain = await this.workspaceService.containsSome(this.workspaceContains); if (!doesContain) { return new Promise(resolve => { }); } return doesContain; } // FIXME move it to the workspace protected waitForOpenTextDocument(selector: DocumentSelector): Promise { const document = this.workspace.textDocuments.filter(doc => this.languages.match(selector, doc) )[0]; if (document !== undefined) { return Promise.resolve(document); } return new Promise(resolve => { const disposable = this.workspace.onDidOpenTextDocument(doc => { if (this.languages.match(selector, doc)) { disposable.dispose(); resolve(doc); } }); }); } }