Skip to content

Commit

Permalink
GLSP-960 Improve Diagram container configuration (eclipse-glsp#274)
Browse files Browse the repository at this point in the history
* GLSP-960 Improve Diagram container configuration

- Make import configuration constants available in DI container via `IDiagramOptions`.
  This includes diagramType, clientId, sourceUri and glspClient.
- Introduce `DiagramLoader` as new central component responsible for configuring the glsp client and dispatching the intial model loading request
- Introduce `IDiagramStartup` services that can hook into the diagram loader lifecycle (GLSP-587)
- This means that feature modules are now self containted and can dispatch their initial actions, activate UI extensions on startup etc. without having to modifiy the `GLSPDiagram` (or similiar) componenent.

-Add `onServerInitialized` event + tests to `GLSPClient`

Part of eclipse-glsp/glsp#960
Part of eclipse-glsp/glsp#587
  • Loading branch information
tortmayr authored and holkerveen committed Dec 21, 2024
1 parent 8fa496f commit 60794c6
Show file tree
Hide file tree
Showing 23 changed files with 401 additions and 159 deletions.
20 changes: 5 additions & 15 deletions examples/workflow-glsp/src/workflow-diagram-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ import {
configureDefaultModelElements,
configureModelElement,
editLabelFeature,
initializeDiagramContainer,
overrideViewerOptions
initializeDiagramContainer
} from '@eclipse-glsp/client';
import 'balloon-css/balloon.min.css';
import { Container, ContainerModule } from 'inversify';
Expand Down Expand Up @@ -78,19 +77,10 @@ export const workflowDiagramModule = new ContainerModule((bind, unbind, isBound,
configureModelElement(context, 'struct', SCompartment, StructureCompartmentView);
});

export function createWorkflowDiagramContainer(widgetId: string, ...containerConfiguration: ContainerConfiguration): Container {
return initializeWorkflowDiagramContainer(new Container(), widgetId, ...containerConfiguration);
export function createWorkflowDiagramContainer(...containerConfiguration: ContainerConfiguration): Container {
return initializeWorkflowDiagramContainer(new Container(), ...containerConfiguration);
}

export function initializeWorkflowDiagramContainer(
container: Container,
widgetId: string,
...containerConfiguration: ContainerConfiguration
): Container {
initializeDiagramContainer(container, workflowDiagramModule, directTaskEditor, accessibilityModule, ...containerConfiguration);
overrideViewerOptions(container, {
baseDiv: widgetId,
hiddenDiv: widgetId + '_hidden'
});
return container;
export function initializeWorkflowDiagramContainer(container: Container, ...containerConfiguration: ContainerConfiguration): Container {
return initializeDiagramContainer(container, workflowDiagramModule, directTaskEditor, accessibilityModule, ...containerConfiguration);
}
57 changes: 12 additions & 45 deletions examples/workflow-standalone/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,15 @@
import 'reflect-metadata';

import {
ApplicationIdProvider,
BaseJsonrpcGLSPClient,
EnableToolPaletteAction,
DiagramLoader,
GLSPActionDispatcher,
GLSPClient,
GLSPModelSource,
GLSPWebSocketProvider,
RequestModelAction,
RequestTypeHintsAction,
ServerMessageAction,
ServerStatusAction,
SetUIExtensionVisibilityAction,
StatusOverlay,
configureServerActions
ServerStatusAction
} from '@eclipse-glsp/client';
import { Container } from 'inversify';
import { join, resolve } from 'path';
import { MessageConnection } from 'vscode-jsonrpc';
import createContainer from './di.config';
Expand All @@ -41,45 +35,21 @@ const diagramType = 'workflow-diagram';
const loc = window.location.pathname;
const currentDir = loc.substring(0, loc.lastIndexOf('/'));
const examplePath = resolve(join(currentDir, '../app/example1.wf'));
const clientId = ApplicationIdProvider.get() + '_' + examplePath;
const clientId = 'sprotty';

const webSocketUrl = `ws://localhost:${port}/${id}`;

let container = createContainer();
let diagramServer = container.get(GLSPModelSource);

let glspClient: GLSPClient;
let container: Container;
const wsProvider = new GLSPWebSocketProvider(webSocketUrl);
wsProvider.listen({ onConnection: initialize, onReconnect: reconnect, logger: console });

async function initialize(connectionProvider: MessageConnection, isReconnecting = false): Promise<void> {
const actionDispatcher: GLSPActionDispatcher = container.get(GLSPActionDispatcher);

await actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: StatusOverlay.ID, visible: true }));
await actionDispatcher.dispatch(ServerStatusAction.create('Initializing...', { severity: 'INFO' }));
const client = new BaseJsonrpcGLSPClient({ id, connectionProvider });

await diagramServer.connect(client, clientId);
const result = await client.initializeServer({
applicationId: ApplicationIdProvider.get(),
protocolVersion: GLSPClient.protocolVersion
});
actionDispatcher.dispatch(ServerStatusAction.create('', { severity: 'NONE' }));
await configureServerActions(result, diagramType, container);

await client.initializeClientSession({ clientSessionId: diagramServer.clientId, diagramType });

actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: StatusOverlay.ID, visible: true }));
actionDispatcher.dispatch(EnableToolPaletteAction.create());
actionDispatcher.dispatch(
RequestModelAction.create({
options: {
sourceUri: `file://${examplePath}`,
diagramType,
isReconnecting
}
})
);
actionDispatcher.dispatch(RequestTypeHintsAction.create());
glspClient = new BaseJsonrpcGLSPClient({ id, connectionProvider });
container = createContainer({ clientId, diagramType, glspClient, sourceUri: examplePath });
const actionDispatcher = container.get(GLSPActionDispatcher);
const diagramLoader = container.get(DiagramLoader);
await diagramLoader.load({ isReconnecting });

if (isReconnecting) {
const message = `Connection to the ${id} glsp server got closed. Connection was successfully re-established.`;
Expand All @@ -94,9 +64,6 @@ async function initialize(connectionProvider: MessageConnection, isReconnecting
}

async function reconnect(connectionProvider: MessageConnection): Promise<void> {
container = createContainer();
diagramServer = container.get(GLSPModelSource);
diagramServer.clientId = clientId;

glspClient.stop();
initialize(connectionProvider, true /* isReconnecting */);
}
14 changes: 11 additions & 3 deletions examples/workflow-standalone/src/di.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { createWorkflowDiagramContainer } from '@eclipse-glsp-examples/workflow-glsp';
import { bindOrRebind, ConsoleLogger, LogLevel, STANDALONE_MODULE_CONFIG, TYPES } from '@eclipse-glsp/client';
import {
bindOrRebind,
ConsoleLogger,
createDiagramOptionsModule,
IDiagramOptions,
LogLevel,
STANDALONE_MODULE_CONFIG,
TYPES
} from '@eclipse-glsp/client';
import { Container } from 'inversify';
import '../css/diagram.css';
export default function createContainer(): Container {
const container = createWorkflowDiagramContainer('sprotty', STANDALONE_MODULE_CONFIG);
export default function createContainer(options: IDiagramOptions): Container {
const container = createWorkflowDiagramContainer(createDiagramOptionsModule(options), STANDALONE_MODULE_CONFIG);
bindOrRebind(container, TYPES.ILogger).to(ConsoleLogger).inSingletonScope();
bindOrRebind(container, TYPES.LogLevel).toConstantValue(LogLevel.warn);
container.bind(TYPES.IMarqueeBehavior).toConstantValue({ entireEdge: true, entireElement: true });
Expand Down
23 changes: 2 additions & 21 deletions packages/client/src/base/default.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,10 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import '@vscode/codicons/dist/codicon.css';
import { Container } from 'inversify';
import {
ActionHandlerRegistry,
FeatureModule,
InitializeResult,
KeyTool,
LocationPostprocessor,
ModelSource,
MouseTool,
MoveCommand,
SetDirtyStateAction,
Expand All @@ -43,6 +39,7 @@ import { FeedbackAwareUpdateModelCommand } from './feedback/update-model-command
import { FocusStateChangedAction } from './focus/focus-state-change-action';
import { FocusTracker } from './focus/focus-tracker';
import { DefaultModelInitializationConstraint, ModelInitializationConstraint } from './model-initialization-constraint';
import { DiagramLoader } from './model/diagram-loader';
import { GLSPModelSource } from './model/glsp-model-source';
import { GLSPModelRegistry } from './model/model-registry';
import { SelectionClearingMouseListener } from './selection-clearing-mouse-listener';
Expand Down Expand Up @@ -97,6 +94,7 @@ export const defaultModule = new FeatureModule((bind, unbind, isBound, rebind, .
bindOrRebind(context, TYPES.IActionDispatcher).toService(GLSPActionDispatcher);

bindAsService(context, TYPES.ModelSource, GLSPModelSource);
bind(DiagramLoader).toSelf().inSingletonScope();
bind(ModelInitializationConstraint).to(DefaultModelInitializationConstraint).inSingletonScope();

// support re-registration of model elements and views
Expand All @@ -115,20 +113,3 @@ export const defaultModule = new FeatureModule((bind, unbind, isBound, rebind, .
bindAsService(context, TYPES.IVNodePostprocessor, LocationPostprocessor);
bind(TYPES.HiddenVNodePostprocessor).toService(LocationPostprocessor);
});

/**
* Utility function to configure the {@link ModelSource}, i.e. the `DiagramServer`, as action handler for all server actions for the given
* diagramType.
* @param result A promise that resolves after all server actions have been registered.
* @param diagramType The diagram type.
* @param container The di container.
*/
export async function configureServerActions(result: InitializeResult, diagramType: string, container: Container): Promise<void> {
const modelSource = container.get<ModelSource>(TYPES.ModelSource);
const actionHandlerRegistry = container.get<ActionHandlerRegistry>(ActionHandlerRegistry);
const serverActions = result.serverActions[diagramType];
if (serverActions.length === 0) {
throw new Error(`No server-handled actions could be derived from the initialize result for diagramType: ${diagramType}!`);
}
serverActions.forEach(actionKind => actionHandlerRegistry.register(actionKind, modelSource));
}
40 changes: 29 additions & 11 deletions packages/client/src/base/editor-context-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import {
EditorContext,
Emitter,
Event,
GLSPClient,
IActionHandler,
ModelSource,
MaybePromise,
MousePositionTracker,
SModelElement,
SModelRoot,
Expand All @@ -33,7 +34,8 @@ import {
TYPES,
ValueChange
} from '~glsp-sprotty';
import { GLSPModelSource } from './model/glsp-model-source';
import { GLSPActionDispatcher } from './action-dispatcher';
import { IDiagramOptions, IDiagramStartup } from './model/diagram-loader';
import { SelectionService } from './selection-service';

export interface IEditModeListener {
Expand All @@ -54,19 +56,22 @@ export type DirtyStateChange = Pick<SetDirtyStateAction, 'isDirty' | 'reason'>;
* position, etc.).
*/
@injectable()
export class EditorContextService implements IActionHandler, Disposable {
export class EditorContextService implements IActionHandler, Disposable, IDiagramStartup {
@inject(SelectionService)
protected selectionService: SelectionService;

@inject(MousePositionTracker)
protected mousePositionTracker: MousePositionTracker;

@inject(TYPES.IDiagramOptions)
protected diagramOptions: IDiagramOptions;

@multiInject(TYPES.IEditModeListener)
@optional()
protected editModeListeners: IEditModeListener[] = [];

@inject(TYPES.ModelSourceProvider)
protected modelSourceProvider: () => Promise<ModelSource>;
@inject(GLSPActionDispatcher)
protected actionDispatcher: GLSPActionDispatcher;

protected _editMode: string;
protected onEditModeChangedEmitter = new Emitter<ValueChange<string>>();
Expand All @@ -84,6 +89,7 @@ export class EditorContextService implements IActionHandler, Disposable {

@postConstruct()
protected initialize(): void {
this._editMode = this.diagramOptions.editMode ?? EditMode.EDITABLE;
this.toDispose.push(this.onEditModeChangedEmitter, this.onDirtyStateChangedEmitter);
this.editModeListeners.forEach(listener =>
this.onEditModeChanged(change => listener.editModeChanged(change.newValue, change.oldValue))
Expand Down Expand Up @@ -132,18 +138,26 @@ export class EditorContextService implements IActionHandler, Disposable {
}
}

async getSourceUri(): Promise<string | undefined> {
const modelSource = await this.modelSourceProvider();
if (modelSource instanceof GLSPModelSource) {
return modelSource.sourceUri;
}
return undefined;
get sourceUri(): string | undefined {
return this.diagramOptions.sourceUri;
}

get editMode(): string {
return this._editMode;
}

get diagramType(): string {
return this.diagramOptions.diagramType;
}

get clientId(): string {
return this.diagramOptions.clientId;
}

get glspClient(): GLSPClient {
return this.diagramOptions.glspClient;
}

get modelRoot(): Readonly<SModelRoot> {
return this.selectionService.getModelRoot();
}
Expand All @@ -159,6 +173,10 @@ export class EditorContextService implements IActionHandler, Disposable {
get isDirty(): boolean {
return this._isDirty;
}

postRequestModel(): MaybePromise<void> {
this.actionDispatcher.dispatch(SetEditModeAction.create(this.editMode));
}
}

export type EditorContextServiceProvider = () => Promise<EditorContextService>;
Loading

0 comments on commit 60794c6

Please sign in to comment.