Skip to content

Commit

Permalink
Refactor and cleanup GLSPContribution API (#146)
Browse files Browse the repository at this point in the history
- Refactor `GLSPCLientProvider` and reuse it in `BaseGLSPTheiaConnector`
- Make GLSPContributions disposable
- Cleanup redundant session concept in `GLSPBackendContribution`
- Cleanup & refactor`GLSPClientContribution` API.
- Cleanup & refactor`GLSPSocketServerContribution` API
- Remove remains from @theia/languages
- Simplify client initialization by using Theia's Deferred concept.

Fixes eclipse-glsp/glsp#848
  • Loading branch information
tortmayr authored Mar 15, 2023
1 parent 00910e5 commit b258494
Show file tree
Hide file tree
Showing 13 changed files with 207 additions and 242 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
- `forward` method now takes a `Channel` as first parameter instead of a `Connection`
- [deps] Switch Theia extension dependencies to peer dependencies. These dependencies are no longer autoresolved and have to be declared
in the application package. [#138](https://github.com/eclipse-glsp/glsp-theia-integration/pull/138) - Contributed on behalf of STMicroelectronics <br>
- [API] Refactor `GLSPContribution` API [#146](https://github.com/eclipse-glsp/glsp-theia-integration/pull/146)
- `GLSPClientContribution.waitForActivation` is now optional and is not implemented by default.
- `GLSPClientProviderImpl` has been renamed to `GLSPClientProvider`, function keys have been renamed has well
- Removed `GLSPContribution.Service` and dropped the related deprecated session concept.

## [1.0.0 - 30/06/2022](https://github.com/eclipse-glsp/glsp-theia-integration/releases/tag/v1.0.0)

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"publish:latest": "lerna publish from-git --no-git-reset --no-git-tag-version --no-verify-access --no-push",
"publish:next": "SHA=$(git rev-parse --short HEAD) && lerna publish preminor --exact --canary --preid next.${SHA} --dist-tag next --no-git-reset --no-git-tag-version --no-push --ignore-scripts --yes --no-verify-access",
"publish:prepare": "lerna version --ignore-scripts --yes --no-push",
"start": "yarn --cwd examples/browser-app start",
"start:debug": "yarn --cwd examples/browser-app start:debug",
"upgrade:next": "yarn upgrade -p \"@eclipse-glsp.*\" --next ",
"watch": "lerna run --parallel watch"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/********************************************************************************
* Copyright (c) 2019-2022 EclipseSource and others.
* Copyright (c) 2019-2023 EclipseSource 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
Expand All @@ -23,15 +23,16 @@ import {
ServerMessageAction,
ServerStatusAction
} from '@eclipse-glsp/client';
import { ContributionProvider, Message, MessageService, MessageType } from '@theia/core';
import { Message, MessageService, MessageType } from '@theia/core';
import { ConfirmDialog, WidgetManager } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { EditorManager } from '@theia/editor/lib/browser';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { DiagramWidget, TheiaDiagramServer } from 'sprotty-theia';
import { GLSPClientContribution } from '../glsp-client-contribution';
import { GLSPClientProvider } from '../glsp-client-provider';
import { deriveDiagramManagerId } from './glsp-diagram-manager';
import { GLSPMessageOptions, GLSPNotificationManager } from './glsp-notification-manager';
import { TheiaGLSPConnector } from './theia-glsp-connector';
Expand All @@ -58,37 +59,38 @@ export abstract class BaseTheiaGLSPConnector implements TheiaGLSPConnector {
@inject(GLSPNotificationManager)
protected readonly notificationManager: GLSPNotificationManager;

@inject(ContributionProvider)
@named(GLSPClientContribution)
protected readonly clientContributions: ContributionProvider<GLSPClientContribution>;
@inject(GLSPClientProvider)
protected readonly glspClientProvider: GLSPClientProvider;

private servers: Map<string, TheiaDiagramServer> = new Map();
private widgetMessages: Map<string, string[]> = new Map();
private widgetStatusTimeouts: Map<string, number> = new Map();

abstract readonly diagramType: string;
abstract readonly contributionId: string;

protected glspClientContribution: GLSPClientContribution;

@postConstruct()
protected initialize(): void {
const contributions = this.clientContributions.getContributions().filter(contribution => contribution.id === this.contributionId);
if (contributions.length === 0) {
throw new Error(`Could not retrieve GLSP client contribution with id '${this.contributionId}}'`);
const clientContribution = this.glspClientProvider.getGLSPClientContribution(this.contributionId);
if (!clientContribution) {
throw new Error(`No GLSPClientContribution is configured for the id '${this.contributionId}'`);
}
this.glspClientContribution = contributions[0];
this.glspClientContribution.glspClient.then(client => client.onActionMessage(this.onMessageReceived.bind(this)));
this.glspClientContribution = clientContribution;
}

connect(diagramServer: TheiaDiagramServer): void {
this.servers.set(diagramServer.clientId, diagramServer);
this.glspClient.then(client =>

this.glspClient.then(client => {
client.onActionMessage(message => this.onMessageReceived(message));
client.initializeClientSession({
clientSessionId: diagramServer.clientId,
diagramType: this.diagramType,
args: this.initializeClientSessionArgs(diagramServer)
})
);
});
});
diagramServer.connect(this);
}

Expand All @@ -113,15 +115,15 @@ export abstract class BaseTheiaGLSPConnector implements TheiaGLSPConnector {

async save(uri: string, action: ExportSvgAction): Promise<void> {
const folder = await this.fileService.resolve(new URI(uri));
let file = await this.fileDialogService.showSaveDialog({ title: 'Export Diagram' , filters: { 'Images (*.svg)': ['svg'] }}, folder);
if(file) {
let file = await this.fileDialogService.showSaveDialog({ title: 'Export Diagram', filters: { 'Images (*.svg)': ['svg'] } }, folder);
if (file) {
try {
if(!file.path.ext) {
if (!file.path.ext) {
file = new URI(file.path.fsPath() + '.svg');
}
await this.fileService.write(file, action.svg);
this.messageService.info(`Diagram exported to '${file.path.name}'`);
} catch(error) {
} catch (error) {
this.messageService.info(`Error exporting diagram '${error}'`);
}
}
Expand Down
164 changes: 72 additions & 92 deletions packages/theia-integration/src/browser/glsp-client-contribution.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/********************************************************************************
* Copyright (C) 2017-2021 TypeFox and others.
* Copyright (C) 2019-2023 EclipseSource 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
Expand All @@ -16,7 +16,6 @@
import {
ApplicationIdProvider,
Args,
ClientState,
ConnectionProvider,
GLSPClient,
InitializeParameters,
Expand All @@ -25,9 +24,8 @@ import {
} from '@eclipse-glsp/client';
import { Disposable, DisposableCollection, MessageService } from '@theia/core';
import { FrontendApplication, WebSocketConnectionProvider } from '@theia/core/lib/browser';
import { inject, injectable, multiInject } from '@theia/core/shared/inversify';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { DiagramManagerProvider } from 'sprotty-theia';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { inject, injectable } from '@theia/core/shared/inversify';
import 'sprotty-theia/css/theia-sprotty.css';
import 'sprotty/css/sprotty.css';
import '../../css/command-palette.css';
Expand All @@ -41,37 +39,65 @@ import { TheiaJsonrpcGLSPClient } from './theia-jsonrpc-glsp-client';

export const GLSPClientContribution = Symbol.for('GLSPClientContribution');

/**
* The frontend service component of a {@link GLSPContribution}. Responsible for providing & initializing the
* {@link GLSPClient}.
*/
export interface GLSPClientContribution extends GLSPContribution {
readonly running: boolean;
readonly initializeResult: Promise<InitializeResult>;
readonly glspClient: Promise<GLSPClient>;
waitForActivation(app: FrontendApplication): Promise<void>;
activate(app: FrontendApplication): Disposable;
/**
* Triggers the setup for the {@link GLSPClient}.
* The activation phase consists of the following steps:
* - Establish a service connection to the corresponding backend contribution (`GLSPServerContribution`)
* - Create a new {@link GLSPClient} on top of the service connection
* - Start the client
* - Initialize the server
*
* The {@link GLSPClientContribution.waitForActivation} function can be used to further delay the activation
* @param app Theia`s frontend application
*/
activate(app: FrontendApplication): MaybePromise<void>;

/**
* Optional function to delay the activation of this client contribution until certain conditions are met
* @param app Theia`s frontend application
* @returns A promise that resolves once all activation conditions are met.
*/
waitForActivation?(app: FrontendApplication): Promise<void>;

/**
* Deactivates the contribution and disposes all underlying resources e.g. the service connection
* and the glsp client.
*
* @param app Theia`s frontend application
*/
deactivate(app: FrontendApplication): void;

/**
* Retrieve the activated {@link GLSPClient}.
* @returns A promise of the client that resolves after the client has been started & initialized
*/
readonly glspClient: Promise<GLSPClient>;

/**
* The cached result of the client initialization
* @returns A promise that will resolve after {@link GLSPClient.initializeServer} has been called
*/
readonly initializeResult: Promise<InitializeResult>;
}

@injectable()
export abstract class BaseGLSPClientContribution implements GLSPClientContribution {
abstract readonly id: string;

protected _glspClient: GLSPClient | undefined;

protected resolveReady: (glspClient: GLSPClient) => void;
protected ready: Promise<GLSPClient>;
protected readonly toDeactivate = new DisposableCollection();
protected glspClientDeferred: Deferred<GLSPClient> = new Deferred();
protected readonly toDispose = new DisposableCollection();
protected _initializeResult: InitializeResult | undefined;

@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(MessageService) protected readonly messageService: MessageService;
@inject(WebSocketConnectionProvider) protected readonly connectionProvider: WebSocketConnectionProvider;
@multiInject(DiagramManagerProvider) protected diagramManagerProviders: DiagramManagerProvider[];

constructor() {
this.waitForReady();
}

get glspClient(): Promise<GLSPClient> {
return this._glspClient ? Promise.resolve(this._glspClient) : this.ready;
return this.glspClientDeferred.promise;
}

get initializeResult(): Promise<InitializeResult> {
Expand All @@ -83,65 +109,41 @@ export abstract class BaseGLSPClientContribution implements GLSPClientContributi
});
}

waitForActivation(app: FrontendApplication): Promise<any> {
const activationPromises: Promise<any>[] = [];
const workspaceContains = this.workspaceContains;
if (workspaceContains.length !== 0) {
activationPromises.push(this.waitForItemInWorkspace());
}
if (activationPromises.length !== 0) {
return Promise.all([
this.ready,
Promise.race(
activationPromises.map(
p =>
// eslint-disable-next-line no-async-promise-executor
new Promise<void>(async resolve => {
try {
await p;
resolve();
} catch (e) {
console.error(e);
}
})
)
)
]);
}
return this.ready;
}
waitForActivation?(app: FrontendApplication): Promise<void>;

activate(): Disposable {
if (this.toDeactivate.disposed) {
activate(app: FrontendApplication): MaybePromise<void> {
if (this.toDispose.disposed) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.toDeactivate.push(new DisposableCollection(Disposable.create(() => {}))); // mark as not disposed
this.doActivate(this.toDeactivate);
this.toDispose.push(new DisposableCollection(Disposable.create(() => {}))); // mark as not disposed
if (this.waitForActivation) {
return this.waitForActivation(app).then(() => this.doActivate());
}
return this.doActivate();
}
return this.toDeactivate;
}

deactivate(_app: FrontendApplication): void {
this.toDeactivate.dispose();
this.dispose();
}

protected async doActivate(toStop: DisposableCollection): Promise<void> {
protected async doActivate(): Promise<void> {
try {
this.connectionProvider.listen(
{
path: GLSPContribution.getPath(this),
onConnection: channel => {
if (toStop.disposed) {
if (this.toDispose.disposed) {
channel.close();
return;
}
const connection = createChannelConnection(channel);
const languageClient = this.createGLSPCLient(connection);
this.onWillStart(languageClient);
toStop.pushAll([
const client = this.createGLSPClient(connection);
this.start(client);
this.toDispose.pushAll([
Disposable.create(() => {
channel.close();
languageClient.shutdownServer();
languageClient.stop();
client.shutdownServer();
client.stop();
})
]);
}
Expand All @@ -150,17 +152,14 @@ export abstract class BaseGLSPClientContribution implements GLSPClientContributi
);
} catch (e) {
console.error(e);
this.glspClientDeferred.reject(e);
}
}

get running(): boolean {
return !this.toDeactivate.disposed && this._glspClient !== undefined && this._glspClient.currentState === ClientState.Running;
}

protected async onWillStart(languageClient: GLSPClient): Promise<void> {
await languageClient.start();
this._initializeResult = await this.initialize(languageClient);
this.onReady(languageClient);
protected async start(glspClient: GLSPClient): Promise<void> {
await glspClient.start();
this._initializeResult = await this.initialize(glspClient);
this.glspClientDeferred.resolve(glspClient);
}

protected async initialize(languageClient: GLSPClient): Promise<InitializeResult> {
Expand All @@ -187,34 +186,15 @@ export abstract class BaseGLSPClientContribution implements GLSPClientContributi
return undefined;
}

protected onReady(languageClient: GLSPClient): void {
this._glspClient = languageClient;
this.resolveReady(this._glspClient);
this.waitForReady();
}

protected waitForReady(): void {
this.ready = new Promise<GLSPClient>(resolve => (this.resolveReady = resolve));
}

protected createGLSPCLient(connectionProvider: ConnectionProvider): GLSPClient {
protected createGLSPClient(connectionProvider: ConnectionProvider): GLSPClient {
return new TheiaJsonrpcGLSPClient({
id: this.id,
connectionProvider,
messageService: this.messageService
});
}

protected get workspaceContains(): string[] {
return [];
}

protected async waitForItemInWorkspace(): Promise<any> {
const doesContain = await this.workspaceService.containsSome(this.workspaceContains);
if (!doesContain) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return new Promise(resolve => {});
}
return doesContain;
dispose(): void {
this.toDispose.dispose();
}
}
Loading

0 comments on commit b258494

Please sign in to comment.