From c0e6a7db76a7fc8b77e46b06af0c58cb189df591 Mon Sep 17 00:00:00 2001 From: Tobias Ortmayr Date: Fri, 15 Sep 2023 03:50:24 -0700 Subject: [PATCH] GLSP-1116 Revise model loading (#287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * GLSP-1116 Revise model loading - Refactor diagram loader - Remove dispatching of temporary empty set model action and instead call `actionDispatcher.initialize()` earlier which also dispatches an empty set model action under the hood - Add additional `postModelInitalization` hook for startup services that want execute logic after the model is fully initialized - Rework `ModelInitializationConstraint` - Provide `onInitialized` override that allows sync registration of listener callbacks - Refactor `setCompleted` method and remove the possiblity to set the initialized state to false. Model initialization is a one-time action. Once initialized there should be no way to "uninitialize" the constraint - Provide test cases - Add `dispatchOnceModelInitialized` utility function to action dispatcher - Ensure that type hints are requested after the model has been initialized Part-of: https://github.com/eclipse-glsp/glsp/issues/1116 Part-of: https://github.com/eclipse-glsp/glsp/issues/606 * GLSP-1117: Remove need for explicit definition of client actions -Extend `initializeClientSession` request to also specific the set of client action kinds. This way the server knows which actions should be sent to the client -Adapt `GLSPModelSource´ to retrieve the client actions before sending the `initializeClientSession` request - Add customized `GLSPActionHandlerRegistry` which provides a query method to retrieve all handled action kinds Part of eclipse-glsp/glsp/issues/1117 * GLSP-1071: Rename ServerStatus/ServerMessage action Part of https://github.com/eclipse-glsp/glsp/issues/1071 --- examples/workflow-standalone/src/app.ts | 9 +- packages/client/src/base/action-dispatcher.ts | 16 +++- .../src/base/action-handler-registry.ts | 30 ++++++ packages/client/src/base/default.module.ts | 6 +- .../client/src/base/model/diagram-loader.ts | 34 ++++--- .../src/base/model/glsp-model-source.ts | 17 +++- .../model-initialization-constraint.spec.ts | 95 +++++++++++++++++++ .../model-initialization-constraint.ts | 53 +++++++++-- .../diagram-navigation-tool.ts | 42 ++++---- .../resize-key-tool/resize-key-tool.ts | 28 +++--- .../accessibility/search/search-tool.ts | 14 ++- .../view-key-tools/movement-key-tool.ts | 14 ++- .../view-key-tools/zoom-key-tool.ts | 24 +++-- .../copy-paste/copy-paste-context-menu.ts | 9 +- .../client/src/features/hints/type-hints.ts | 2 +- .../label-edit/edit-label-validator.ts | 8 +- .../navigation/navigation-action-handler.ts | 13 +-- .../source-model-changed-action-handler.ts | 9 +- .../src/features/status/status-module.ts | 4 +- .../src/features/status/status-overlay.ts | 10 +- packages/client/src/index.ts | 15 +-- packages/client/src/standalone-modules.ts | 4 +- .../src/action-protocol/base-protocol.ts | 5 +- .../client-notification.spec.ts | 48 ++++------ .../action-protocol/client-notification.ts | 46 ++++----- .../base-glsp-client.spec.ts | 6 +- .../jsonrpc/base-jsonrpc-glsp-client.spec.ts | 4 +- .../src/client-server-protocol/types.ts | 6 ++ 28 files changed, 371 insertions(+), 200 deletions(-) create mode 100644 packages/client/src/base/action-handler-registry.ts create mode 100644 packages/client/src/base/model/model-initialization-constraint.spec.ts rename packages/client/src/base/{ => model}/model-initialization-constraint.ts (65%) diff --git a/examples/workflow-standalone/src/app.ts b/examples/workflow-standalone/src/app.ts index 78d3ff4f..aeea6343 100644 --- a/examples/workflow-standalone/src/app.ts +++ b/examples/workflow-standalone/src/app.ts @@ -21,8 +21,8 @@ import { GLSPActionDispatcher, GLSPClient, GLSPWebSocketProvider, - ServerMessageAction, - ServerStatusAction + MessageAction, + StatusAction } from '@eclipse-glsp/client'; import { Container } from 'inversify'; import { join, resolve } from 'path'; @@ -55,10 +55,7 @@ async function initialize(connectionProvider: MessageConnection, isReconnecting const message = `Connection to the ${id} glsp server got closed. Connection was successfully re-established.`; const timeout = 5000; const severity = 'WARNING'; - actionDispatcher.dispatchAll([ - ServerStatusAction.create(message, { severity, timeout }), - ServerMessageAction.create(message, { severity }) - ]); + actionDispatcher.dispatchAll([StatusAction.create(message, { severity, timeout }), MessageAction.create(message, { severity })]); return; } } diff --git a/packages/client/src/base/action-dispatcher.ts b/packages/client/src/base/action-dispatcher.ts index 6af92dcd..40349cda 100644 --- a/packages/client/src/base/action-dispatcher.ts +++ b/packages/client/src/base/action-dispatcher.ts @@ -15,14 +15,15 @@ ********************************************************************************/ import { inject, injectable } from 'inversify'; import { Action, ActionDispatcher, RequestAction, ResponseAction } from '~glsp-sprotty'; -import { ModelInitializationConstraint } from './model-initialization-constraint'; +import { ModelInitializationConstraint } from './model/model-initialization-constraint'; @injectable() export class GLSPActionDispatcher extends ActionDispatcher { protected readonly timeouts: Map = new Map(); protected initializedConstraint = false; - @inject(ModelInitializationConstraint) protected initializationConstraint: ModelInitializationConstraint; + @inject(ModelInitializationConstraint) + protected initializationConstraint: ModelInitializationConstraint; override initialize(): Promise { return super.initialize().then(() => this.startModelInitialization()); @@ -31,7 +32,7 @@ export class GLSPActionDispatcher extends ActionDispatcher { startModelInitialization(): void { if (!this.initializedConstraint) { this.logger.log(this, 'Starting model initialization mode'); - this.initializationConstraint.onInitialized().then(() => this.logger.log(this, 'Model initialization completed')); + this.initializationConstraint.onInitialized(() => this.logger.log(this, 'Model initialization completed')); this.initializedConstraint = true; } } @@ -44,6 +45,15 @@ export class GLSPActionDispatcher extends ActionDispatcher { return this.actionHandlerRegistry.get(action.kind).length > 0; } + /** + * Processes all given actions, by dispatching them to the corresponding handlers, after the model initialization is completed. + * + * @param actions The actions that should be dispatched after the model initialization + */ + dispatchOnceModelInitialized(...actions: Action[]): void { + this.initializationConstraint.onInitialized(() => this.dispatchAll(actions)); + } + override dispatch(action: Action): Promise { const result = super.dispatch(action); this.initializationConstraint.notifyDispatched(action); diff --git a/packages/client/src/base/action-handler-registry.ts b/packages/client/src/base/action-handler-registry.ts new file mode 100644 index 00000000..e4220bb4 --- /dev/null +++ b/packages/client/src/base/action-handler-registry.ts @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 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 + * 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 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import { ActionHandlerRegistry } from '~glsp-sprotty'; + +@injectable() +export class GLSPActionHandlerRegistry extends ActionHandlerRegistry { + /** + * Retrieve a set of all action kinds for which (at least) one + * handler is registered + * @returns the set of handled action kinds + */ + getHandledActionKinds(): string[] { + return Array.from(this.elements.keys()); + } +} diff --git a/packages/client/src/base/default.module.ts b/packages/client/src/base/default.module.ts index 6f20ea09..78bf8e2b 100644 --- a/packages/client/src/base/default.module.ts +++ b/packages/client/src/base/default.module.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import '@vscode/codicons/dist/codicon.css'; import { + ActionHandlerRegistry, FeatureModule, KeyTool, LocationPostprocessor, @@ -31,6 +32,7 @@ import { } from '~glsp-sprotty'; import '../../css/glsp-sprotty.css'; import { GLSPActionDispatcher } from './action-dispatcher'; +import { GLSPActionHandlerRegistry } from './action-handler-registry'; import { GLSPCommandStack } from './command-stack'; import { EditorContextService } from './editor-context-service'; import { ModifyCssFeedbackCommand } from './feedback/css-feedback'; @@ -38,9 +40,9 @@ import { FeedbackActionDispatcher } from './feedback/feedback-action-dispatcher' 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 { DefaultModelInitializationConstraint, ModelInitializationConstraint } from './model/model-initialization-constraint'; import { GLSPModelRegistry } from './model/model-registry'; import { SelectionClearingMouseListener } from './selection-clearing-mouse-listener'; import { SelectionService } from './selection-service'; @@ -91,6 +93,8 @@ export const defaultModule = new FeatureModule((bind, unbind, isBound, rebind, . bindOrRebind(context, TYPES.ICommandStack).to(GLSPCommandStack).inSingletonScope(); bind(GLSPActionDispatcher).toSelf().inSingletonScope(); bindOrRebind(context, TYPES.IActionDispatcher).toService(GLSPActionDispatcher); + bind(GLSPActionHandlerRegistry).toSelf().inSingletonScope(); + bindOrRebind(context, ActionHandlerRegistry).toService(GLSPActionHandlerRegistry); bindAsService(context, TYPES.ModelSource, GLSPModelSource); bind(DiagramLoader).toSelf().inSingletonScope(); diff --git a/packages/client/src/base/model/diagram-loader.ts b/packages/client/src/base/model/diagram-loader.ts index 885bbd7d..8112de0f 100644 --- a/packages/client/src/base/model/diagram-loader.ts +++ b/packages/client/src/base/model/diagram-loader.ts @@ -24,14 +24,15 @@ import { InitializeParameters, MaybePromise, RequestModelAction, - ServerStatusAction, SetModelAction, + StatusAction, TYPES, hasNumberProp } from '~glsp-sprotty'; import { GLSPActionDispatcher } from '../action-dispatcher'; import { Ranked } from '../ranked'; import { GLSPModelSource } from './glsp-model-source'; +import { ModelInitializationConstraint } from './model-initialization-constraint'; /** * Configuration options for a specific GLSP diagram instance. @@ -79,9 +80,13 @@ export interface IDiagramStartup extends Partial { * Hook for services that should be executed after the initial model loading request (i.e. `RequestModelAction`). * Note that this hook is invoked directly after the `RequestModelAction` has been dispatched. It does not necessarily wait * until the client-server update roundtrip is completed. If you need to wait until the diagram is fully initialized use the - * {@link GLSPActionDispatcher.onceModelInitialized} constraint. + * {@link postModelInitialization} hook. */ postRequestModel?(): MaybePromise; + /* Hook for services that should be executed after the diagram model is fully initialized + * (i.e. `ModelInitializationConstraint` is completed). + */ + postModelInitialization?(): MaybePromise; } export namespace IDiagramStartup { @@ -89,7 +94,10 @@ export namespace IDiagramStartup { return ( AnyObject.is(object) && hasNumberProp(object, 'rank', true) && - ('preInitialize' in object || 'preRequestModel' in object || 'postRequestModel' in object) + ('preInitialize' in object || + 'preRequestModel' in object || + 'postRequestModel' in object || + 'postModelInitialization' in object) ); } } @@ -142,6 +150,9 @@ export class DiagramLoader { @inject(GLSPModelSource) protected modelSource: GLSPModelSource; + @inject(ModelInitializationConstraint) + protected modelInitializationConstraint: ModelInitializationConstraint; + @postConstruct() protected postConstruct(): void { this.diagramStartups.sort((a, b) => Ranked.getRank(a) - Ranked.getRank(b)); @@ -161,6 +172,7 @@ export class DiagramLoader { }, enableNotifications: options.enableNotifications ?? true }; + await this.actionDispatcher.initialize(); // Set placeholder model until real model from server is available await this.actionDispatcher.dispatch(SetModelAction.create(EMPTY_ROOT)); await this.invokeStartupHook('preInitialize'); @@ -168,6 +180,7 @@ export class DiagramLoader { await this.invokeStartupHook('preRequestModel'); await this.requestModel(resolvedOptions); await this.invokeStartupHook('postRequestModel'); + this.modelInitializationConstraint.onInitialized(() => this.invokeStartupHook('postModelInitialization')); } protected async invokeStartupHook(hook: keyof Omit): Promise { @@ -176,18 +189,13 @@ export class DiagramLoader { } } - protected requestModel(options: ResolvedDiagramLoadingOptions): Promise { - const result = this.actionDispatcher.dispatch(RequestModelAction.create({ options: options.requestModelOptions })); - if (options.enableNotifications) { - this.actionDispatcher.dispatch(ServerStatusAction.create('', { severity: 'NONE' })); - } - return result; + protected async requestModel(options: ResolvedDiagramLoadingOptions): Promise { + return this.actionDispatcher.dispatch(RequestModelAction.create({ options: options.requestModelOptions })); } protected async initialize(options: ResolvedDiagramLoadingOptions): Promise { - await this.actionDispatcher.initialize(); if (options.enableNotifications) { - this.actionDispatcher.dispatch(ServerStatusAction.create('Initializing...', { severity: 'INFO' })); + this.actionDispatcher.dispatch(StatusAction.create('Initializing...', { severity: 'INFO' })); } const glspClient = await this.options.glspClientProvider(); @@ -197,5 +205,9 @@ export class DiagramLoader { if (!glspClient.initializeResult) { await glspClient.initializeServer(options.initializeParameters); } + + if (options.enableNotifications) { + this.actionDispatcher.dispatch(StatusAction.create('', { severity: 'NONE' })); + } } } diff --git a/packages/client/src/base/model/glsp-model-source.ts b/packages/client/src/base/model/glsp-model-source.ts index cdac1885..c7b0bf8a 100644 --- a/packages/client/src/base/model/glsp-model-source.ts +++ b/packages/client/src/base/model/glsp-model-source.ts @@ -17,7 +17,6 @@ import { inject, injectable, postConstruct, preDestroy } from 'inversify'; import { Action, - ActionHandlerRegistry, ActionMessage, Disposable, DisposableCollection, @@ -28,6 +27,7 @@ import { SModelRootSchema, TYPES } from '~glsp-sprotty'; +import { GLSPActionHandlerRegistry } from '../action-handler-registry'; import { IDiagramOptions } from './diagram-loader'; /** * A helper interface that allows the client to mark actions that have been received from the server. @@ -91,14 +91,23 @@ export class GLSPModelSource extends ModelSource implements Disposable { this.clientId = this.options.clientId ?? this.viewerOptions.baseDiv; } - configure(registry: ActionHandlerRegistry, initializeResult: InitializeResult): Promise { + configure(registry: GLSPActionHandlerRegistry, initializeResult: InitializeResult): Promise { const serverActions = initializeResult.serverActions[this.diagramType]; if (!serverActions || serverActions.length === 0) { throw new Error(`No server-handled actions could be derived from the initialize result for diagramType: ${this.diagramType}!`); } + // Retrieve all currently handled action kinds. We do this before registering the server actions + // to ensure that the array will only contain client-side handled actions + const clientActionKinds = registry.getHandledActionKinds(); + serverActions.forEach(action => registry.register(action, this)); this.toDispose.push(this.glspClient!.onActionMessage(message => this.messageReceived(message), this.clientId)); - return this.glspClient!.initializeClientSession({ clientSessionId: this.clientId, diagramType: this.diagramType }); + + return this.glspClient!.initializeClientSession({ + clientSessionId: this.clientId, + clientActionKinds, + diagramType: this.diagramType + }); } protected messageReceived(message: ActionMessage): void { @@ -111,7 +120,7 @@ export class GLSPModelSource extends ModelSource implements Disposable { this.actionDispatcher.dispatch(action); } - override initialize(registry: ActionHandlerRegistry): void { + override initialize(registry: GLSPActionHandlerRegistry): void { // Registering actions here is discouraged and it's recommended // to implemented dedicated action handlers. if (!this.clientId) { diff --git a/packages/client/src/base/model/model-initialization-constraint.spec.ts b/packages/client/src/base/model/model-initialization-constraint.spec.ts new file mode 100644 index 00000000..712a1df6 --- /dev/null +++ b/packages/client/src/base/model/model-initialization-constraint.spec.ts @@ -0,0 +1,95 @@ +/******************************************************************************** + * Copyright (c) 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 + * 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 + ********************************************************************************/ +import { expect } from 'chai'; +import { Container } from 'inversify'; +import 'reflect-metadata'; +import * as sinon from 'sinon'; +import { Deferred, EMPTY_ROOT, InitializeCanvasBoundsAction, SetModelAction, UpdateModelAction } from '~glsp-sprotty'; +import { DefaultModelInitializationConstraint, ModelInitializationConstraint } from './model-initialization-constraint'; +const sandbox = sinon.createSandbox(); +const container = new Container(); +let constraint: ModelInitializationConstraint; +// eslint-disable-next-line @typescript-eslint/no-empty-function +const listener = sandbox.spy((): void => {}); + +describe('DefaultModelInitializationConstraint', () => { + beforeEach(() => { + constraint = container.resolve(DefaultModelInitializationConstraint); + sandbox.reset(); + }); + it('should complete after dispatching non empty SetModelAction and `InitializeCanvasBoundsAction`', () => { + expect(constraint.isCompleted).to.be.false; + constraint.notifyDispatched(SetModelAction.create({ id: 'model', type: 'graph' })); + expect(constraint.isCompleted).to.be.false; + constraint.notifyDispatched({ kind: InitializeCanvasBoundsAction.KIND }); + expect(constraint.isCompleted).to.be.true; + }); + it('should complete after dispatching non empty UpdateModelAction and `InitializeCanvasBoundsAction`', () => { + expect(constraint.isCompleted).to.be.false; + constraint.notifyDispatched(UpdateModelAction.create({ id: 'model', type: 'graph' })); + expect(constraint.isCompleted).to.be.false; + constraint.notifyDispatched({ kind: InitializeCanvasBoundsAction.KIND }); + expect(constraint.isCompleted).to.be.true; + }); + it('should note complete after dispatching empty SetModelAction and `InitializeCanvasBoundsAction` ', () => { + expect(constraint.isCompleted).to.be.false; + constraint.notifyDispatched(SetModelAction.create(EMPTY_ROOT)); + expect(constraint.isCompleted).to.be.false; + constraint.notifyDispatched({ kind: InitializeCanvasBoundsAction.KIND }); + expect(constraint.isCompleted).to.be.false; + }); + it('should note complete after dispatching empty UpdateModelAction and `InitializeCanvasBoundsAction ', () => { + expect(constraint.isCompleted).to.be.false; + constraint.notifyDispatched(UpdateModelAction.create(EMPTY_ROOT)); + expect(constraint.isCompleted).to.be.false; + constraint.notifyDispatched({ kind: InitializeCanvasBoundsAction.KIND }); + expect(constraint.isCompleted).to.be.false; + }); + describe('onInitialized', () => { + it('returned promise should resolve once the constraint is initialized', async () => { + const initializeDeferred = new Deferred(); + const initializePromise = constraint.onInitialized(); + initializePromise.then(() => initializeDeferred.resolve()); + expect(initializeDeferred.state).to.be.equal('unresolved'); + // Directly trigger the completion method simplify test logic + constraint['setCompleted'](); + // Short delay of test execution to ensure that the deferred state is updated. + await new Promise(resolve => setTimeout(resolve, 5)); + expect(initializeDeferred.state).to.be.equal('resolved'); + }); + it('registered listener should be invoked once the constraint is initialized', () => { + constraint.onInitialized(listener); + expect(listener.called).to.be.false; + // Directly trigger the completion method simplify test logic + constraint['setCompleted'](); + expect(listener.called).to.be.true; + }); + it('registered listener should be invoked directly on registration if the constraint is already initialized', () => { + // Directly trigger the completion method simplify test logic + constraint['setCompleted'](); + constraint.onInitialized(listener); + expect(listener.called).to.be.true; + }); + it('Disposed listener should not be invoked once the constraint is initialized', () => { + const toDispose = constraint.onInitialized(listener); + expect(listener.called).to.be.false; + toDispose.dispose(); + // Directly trigger the completion method simplify test logic + constraint['setCompleted'](); + expect(listener.called).to.be.false; + }); + }); +}); diff --git a/packages/client/src/base/model-initialization-constraint.ts b/packages/client/src/base/model/model-initialization-constraint.ts similarity index 65% rename from packages/client/src/base/model-initialization-constraint.ts rename to packages/client/src/base/model/model-initialization-constraint.ts index 67021a4b..99143214 100644 --- a/packages/client/src/base/model-initialization-constraint.ts +++ b/packages/client/src/base/model/model-initialization-constraint.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { injectable } from 'inversify'; -import { Action, Deferred, InitializeCanvasBoundsAction, SetModelAction, UpdateModelAction } from '~glsp-sprotty'; +import { Action, Deferred, Disposable, Emitter, InitializeCanvasBoundsAction, SetModelAction, UpdateModelAction } from '~glsp-sprotty'; /** * The constraint defining when the initialization of the GLSP model is completed. @@ -37,21 +37,46 @@ import { Action, Deferred, InitializeCanvasBoundsAction, SetModelAction, UpdateM @injectable() export abstract class ModelInitializationConstraint { protected completion: Deferred = new Deferred(); - protected completed = false; + protected _isCompleted = false; get isCompleted(): boolean { - return this.completed; + return this._isCompleted; } - protected setCompleted(isCompleted: boolean): void { - this.completed = isCompleted; - if (isCompleted) { - this.completion.resolve(); + protected onInitializedEmitter = new Emitter(); + + /** + * Register a listener that will be invoked once the initialization process + * has been completed. If the initialization is already completed on registration + * the given listener will be invoked right away + * @param listener + */ + onInitialized(listener: () => void): Disposable; + + /** + * Retrieve a promise that resolves once the initialization process + * has been completed. + * @returns the initialization promise + */ + onInitialized(): Promise; + onInitialized(listener?: () => void): Promise | Disposable { + if (!listener) { + return this.completion.promise; } + if (this.isCompleted) { + listener(); + return Disposable.empty(); + } + return this.onInitializedEmitter.event(listener); } - onInitialized(): Promise { - return this.completion.promise; + protected setCompleted(): void { + if (!this.isCompleted) { + this._isCompleted = true; + this.completion.resolve(); + this.onInitializedEmitter.fire(); + this.onInitializedEmitter.dispose(); + } } notifyDispatched(action: Action): void { @@ -59,10 +84,18 @@ export abstract class ModelInitializationConstraint { return; } if (this.isInitializedAfter(action)) { - this.setCompleted(true); + this.setCompleted(); } } + /** + * Central method to check the initialization state. Is invoked + * for every action dispatched by the `ActionDispatcher` (until the initialization has completed). + * Should + * return `true` once the action has been passed which marks the end + * of the initialization process. + * @param action The last dispatched action + */ abstract isInitializedAfter(action: Action): boolean; } diff --git a/packages/client/src/features/accessibility/element-navigation/diagram-navigation-tool.ts b/packages/client/src/features/accessibility/element-navigation/diagram-navigation-tool.ts index 4a49833c..0edab932 100644 --- a/packages/client/src/features/accessibility/element-navigation/diagram-navigation-tool.ts +++ b/packages/client/src/features/accessibility/element-navigation/diagram-navigation-tool.ts @@ -86,28 +86,26 @@ export class ElementNavigatorKeyListener extends KeyListener { } registerShortcutKey(): void { - this.tool.actionDispatcher.onceModelInitialized().then(() => { - this.tool.actionDispatcher.dispatchAll([ - SetAccessibleKeyShortcutAction.create({ - token: this.token, - keys: [ - { shortcuts: ['N'], description: 'Activate default navigation', group: 'Navigation', position: 0 }, - { - shortcuts: ['ALT', 'N'], - description: 'Activate position based navigation', - group: 'Navigation', - position: 1 - }, - { - shortcuts: ['⬅ ⬆ ➡ ⬇'], - description: 'Navigate by relation or neighbors according to navigation mode', - group: 'Navigation', - position: 2 - } - ] - }) - ]); - }); + this.tool.actionDispatcher.dispatchOnceModelInitialized( + SetAccessibleKeyShortcutAction.create({ + token: this.token, + keys: [ + { shortcuts: ['N'], description: 'Activate default navigation', group: 'Navigation', position: 0 }, + { + shortcuts: ['ALT', 'N'], + description: 'Activate position based navigation', + group: 'Navigation', + position: 1 + }, + { + shortcuts: ['⬅ ⬆ ➡ ⬇'], + description: 'Navigate by relation or neighbors according to navigation mode', + group: 'Navigation', + position: 2 + } + ] + }) + ); } override keyDown(element: SModelElement, event: KeyboardEvent): Action[] { diff --git a/packages/client/src/features/accessibility/resize-key-tool/resize-key-tool.ts b/packages/client/src/features/accessibility/resize-key-tool/resize-key-tool.ts index 5d992658..7ee776d6 100644 --- a/packages/client/src/features/accessibility/resize-key-tool/resize-key-tool.ts +++ b/packages/client/src/features/accessibility/resize-key-tool/resize-key-tool.ts @@ -22,9 +22,9 @@ import { SelectionService } from '../../../base/selection-service'; import { EnableDefaultToolsAction, EnableToolsAction, Tool } from '../../../base/tool-manager/tool'; import { IMovementRestrictor } from '../../change-bounds/movement-restrictor'; import { AccessibleKeyShortcutProvider, SetAccessibleKeyShortcutAction } from '../key-shortcut/accessible-key-shortcut'; -import { ResizeElementAction, ResizeType } from './resize-key-handler'; -import { ShowToastMessageAction } from '../toast/toast-handler'; import * as messages from '../toast/messages.json'; +import { ShowToastMessageAction } from '../toast/toast-handler'; +import { ResizeElementAction, ResizeType } from './resize-key-handler'; @injectable() export class ResizeKeyTool implements Tool { @@ -63,19 +63,17 @@ export class ResizeKeyListener extends KeyListener implements AccessibleKeyShort } registerShortcutKey(): void { - this.tool.actionDispatcher.onceModelInitialized().then(() => { - this.tool.actionDispatcher.dispatchAll([ - SetAccessibleKeyShortcutAction.create({ - token: this.token, - keys: [ - { shortcuts: ['ALT', 'A'], description: 'Activate resize mode for selected element', group: 'Resize', position: 0 }, - { shortcuts: ['+'], description: 'Increase size of element', group: 'Resize', position: 1 }, - { shortcuts: ['-'], description: 'Increase size of element', group: 'Resize', position: 2 }, - { shortcuts: ['CTRL', '0'], description: 'Set element size to default', group: 'Resize', position: 3 } - ] - }) - ]); - }); + this.tool.actionDispatcher.dispatchOnceModelInitialized( + SetAccessibleKeyShortcutAction.create({ + token: this.token, + keys: [ + { shortcuts: ['ALT', 'A'], description: 'Activate resize mode for selected element', group: 'Resize', position: 0 }, + { shortcuts: ['+'], description: 'Increase size of element', group: 'Resize', position: 1 }, + { shortcuts: ['-'], description: 'Increase size of element', group: 'Resize', position: 2 }, + { shortcuts: ['CTRL', '0'], description: 'Set element size to default', group: 'Resize', position: 3 } + ] + }) + ); } override keyDown(element: SModelElement, event: KeyboardEvent): Action[] { diff --git a/packages/client/src/features/accessibility/search/search-tool.ts b/packages/client/src/features/accessibility/search/search-tool.ts index e23a9f3e..b295d300 100644 --- a/packages/client/src/features/accessibility/search/search-tool.ts +++ b/packages/client/src/features/accessibility/search/search-tool.ts @@ -51,14 +51,12 @@ export class SearchAutocompletePaletteKeyListener extends KeyListener implements } registerShortcutKey(): void { - this.tool.actionDispatcher.onceModelInitialized().then(() => { - this.tool.actionDispatcher.dispatchAll([ - SetAccessibleKeyShortcutAction.create({ - token: this.token, - keys: [{ shortcuts: ['CTRL', 'F'], description: 'Activate search for elements', group: 'Search', position: 0 }] - }) - ]); - }); + this.tool.actionDispatcher.dispatchOnceModelInitialized( + SetAccessibleKeyShortcutAction.create({ + token: this.token, + keys: [{ shortcuts: ['CTRL', 'F'], description: 'Activate search for elements', group: 'Search', position: 0 }] + }) + ); } override keyDown(element: SModelElement, event: KeyboardEvent): Action[] { diff --git a/packages/client/src/features/accessibility/view-key-tools/movement-key-tool.ts b/packages/client/src/features/accessibility/view-key-tools/movement-key-tool.ts index 12d3b5b1..c41389f2 100644 --- a/packages/client/src/features/accessibility/view-key-tools/movement-key-tool.ts +++ b/packages/client/src/features/accessibility/view-key-tools/movement-key-tool.ts @@ -74,14 +74,12 @@ export class MoveKeyListener extends KeyListener implements AccessibleKeyShortcu } registerShortcutKey(): void { - this.tool.actionDispatcher.onceModelInitialized().then(() => { - this.tool.actionDispatcher.dispatchAll([ - SetAccessibleKeyShortcutAction.create({ - token: this.token, - keys: [{ shortcuts: ['⬅ ⬆ ➡ ⬇'], description: 'Move element or viewport', group: 'Move', position: 0 }] - }) - ]); - }); + this.tool.actionDispatcher.dispatchOnceModelInitialized( + SetAccessibleKeyShortcutAction.create({ + token: this.token, + keys: [{ shortcuts: ['⬅ ⬆ ➡ ⬇'], description: 'Move element or viewport', group: 'Move', position: 0 }] + }) + ); } override keyDown(element: SModelElement, event: KeyboardEvent): Action[] { diff --git a/packages/client/src/features/accessibility/view-key-tools/zoom-key-tool.ts b/packages/client/src/features/accessibility/view-key-tools/zoom-key-tool.ts index 7018c4aa..eb2b3d0d 100644 --- a/packages/client/src/features/accessibility/view-key-tools/zoom-key-tool.ts +++ b/packages/client/src/features/accessibility/view-key-tools/zoom-key-tool.ts @@ -62,19 +62,17 @@ export class ZoomKeyListener extends KeyListener { } registerShortcutKey(): void { - this.tool.actionDispatcher.onceModelInitialized().then(() => { - this.tool.actionDispatcher.dispatchAll([ - SetAccessibleKeyShortcutAction.create({ - token: this.token, - keys: [ - { shortcuts: ['+'], description: 'Zoom in to element or viewport', group: 'Zoom', position: 0 }, - { shortcuts: ['-'], description: 'Zoom out to element or viewport', group: 'Zoom', position: 1 }, - { shortcuts: ['CTRL', '0'], description: 'Reset zoom to default', group: 'Zoom', position: 2 }, - { shortcuts: ['CTRL', '+'], description: 'Zoom in via Grid', group: 'Zoom', position: 3 } - ] - }) - ]); - }); + this.tool.actionDispatcher.dispatchOnceModelInitialized( + SetAccessibleKeyShortcutAction.create({ + token: this.token, + keys: [ + { shortcuts: ['+'], description: 'Zoom in to element or viewport', group: 'Zoom', position: 0 }, + { shortcuts: ['-'], description: 'Zoom out to element or viewport', group: 'Zoom', position: 1 }, + { shortcuts: ['CTRL', '0'], description: 'Reset zoom to default', group: 'Zoom', position: 2 }, + { shortcuts: ['CTRL', '+'], description: 'Zoom in via Grid', group: 'Zoom', position: 3 } + ] + }) + ); } override keyDown(element: SModelElement, event: KeyboardEvent): Action[] { diff --git a/packages/client/src/features/copy-paste/copy-paste-context-menu.ts b/packages/client/src/features/copy-paste/copy-paste-context-menu.ts index 9069715e..b1ef2425 100644 --- a/packages/client/src/features/copy-paste/copy-paste-context-menu.ts +++ b/packages/client/src/features/copy-paste/copy-paste-context-menu.ts @@ -23,8 +23,8 @@ import { MenuItem, Point, SModelRoot, - ServerMessageAction, - ServerStatusAction, + MessageAction, + StatusAction, TYPES, hasStringProp, isSelected @@ -83,10 +83,7 @@ export class InvokeCopyPasteActionHandler implements IActionHandler { const message = `Please use the browser's ${operation} command or shortcut.`; const timeout = 10000; const severity = 'WARNING'; - this.dispatcher.dispatchAll([ - ServerStatusAction.create(message, { severity, timeout }), - ServerMessageAction.create(message, { severity }) - ]); + this.dispatcher.dispatchAll([StatusAction.create(message, { severity, timeout }), MessageAction.create(message, { severity })]); } } diff --git a/packages/client/src/features/hints/type-hints.ts b/packages/client/src/features/hints/type-hints.ts index 67492ce9..4b8a09b5 100644 --- a/packages/client/src/features/hints/type-hints.ts +++ b/packages/client/src/features/hints/type-hints.ts @@ -200,7 +200,7 @@ export class TypeHintProvider implements IActionHandler, ITypeHintProvider, IDia return getTypeHint(input, this.edgeHints); } - preRequestModel(): MaybePromise { + postModelInitialization(): MaybePromise { this.actionDispatcher.dispatch(RequestTypeHintsAction.create()); } } diff --git a/packages/client/src/features/label-edit/edit-label-validator.ts b/packages/client/src/features/label-edit/edit-label-validator.ts index 8a9e4908..32e8ba75 100644 --- a/packages/client/src/features/label-edit/edit-label-validator.ts +++ b/packages/client/src/features/label-edit/edit-label-validator.ts @@ -34,11 +34,11 @@ export namespace LabelEditValidation { export function toEditLabelValidationResult(status: ValidationStatus): EditLabelValidationResult { const message = status.message; - let severity = 'ok' as Severity; + let severity: Severity = 'ok'; if (ValidationStatus.isError(status)) { - severity = 'error' as Severity; + severity = 'error'; } else if (ValidationStatus.isWarning(status)) { - severity = 'warning' as Severity; + severity = 'warning'; } return { message, severity }; } @@ -61,7 +61,7 @@ export class ServerEditLabelValidator implements IEditLabelValidator { if (SetEditValidationResultAction.is(action)) { return LabelEditValidation.toEditLabelValidationResult(action.status); } - return { severity: 'ok' as Severity }; + return { severity: 'ok' }; } } diff --git a/packages/client/src/features/navigation/navigation-action-handler.ts b/packages/client/src/features/navigation/navigation-action-handler.ts index 11e0c3b5..b8944b8a 100644 --- a/packages/client/src/features/navigation/navigation-action-handler.ts +++ b/packages/client/src/features/navigation/navigation-action-handler.ts @@ -23,17 +23,17 @@ import { IActionHandler, ICommand, ILogger, + MessageAction, NavigateToExternalTargetAction, NavigateToTargetAction, NavigationTarget, RequestNavigationTargetsAction, SelectAction, SelectAllAction, - ServerMessageAction, - ServerSeverity, - ServerStatusAction, SetNavigationTargetsAction, SetResolvedNavigationTargetAction, + SeverityLevel, + StatusAction, TYPES, hasObjectProp, hasStringProp @@ -264,11 +264,8 @@ export class NavigationActionHandler implements IActionHandler { this.notify('WARNING', message); } - private notify(severity: ServerSeverity, message: string): void { + private notify(severity: SeverityLevel, message: string): void { const timeout = this.notificationTimeout; - this.dispatcher.dispatchAll([ - ServerStatusAction.create(message, { severity, timeout }), - ServerMessageAction.create(message, { severity }) - ]); + this.dispatcher.dispatchAll([StatusAction.create(message, { severity, timeout }), MessageAction.create(message, { severity })]); } } diff --git a/packages/client/src/features/source-model-watcher/source-model-changed-action-handler.ts b/packages/client/src/features/source-model-watcher/source-model-changed-action-handler.ts index 8cd79a8e..c586ae38 100644 --- a/packages/client/src/features/source-model-watcher/source-model-changed-action-handler.ts +++ b/packages/client/src/features/source-model-watcher/source-model-changed-action-handler.ts @@ -18,9 +18,9 @@ import { Action, IActionDispatcher, IActionHandler, - ServerMessageAction, - ServerStatusAction, + MessageAction, SourceModelChangedAction, + StatusAction, TYPES, ViewerOptions } from '~glsp-sprotty'; @@ -68,9 +68,6 @@ export class SourceModelChangedActionHandler implements IActionHandler { const message = `The source model ${action.sourceModelName} has changed. You might want to consider reloading.`; const timeout = 0; const severity = 'WARNING'; - this.dispatcher.dispatchAll([ - ServerStatusAction.create(message, { severity, timeout }), - ServerMessageAction.create(message, { severity }) - ]); + this.dispatcher.dispatchAll([StatusAction.create(message, { severity, timeout }), MessageAction.create(message, { severity })]); } } diff --git a/packages/client/src/features/status/status-module.ts b/packages/client/src/features/status/status-module.ts index 53520d47..6b1d7c18 100644 --- a/packages/client/src/features/status/status-module.ts +++ b/packages/client/src/features/status/status-module.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { FeatureModule, ServerStatusAction, TYPES, bindAsService, configureActionHandler } from '~glsp-sprotty'; +import { FeatureModule, StatusAction, TYPES, bindAsService, configureActionHandler } from '~glsp-sprotty'; import '../../../css/status-overlay.css'; import { StatusOverlay } from './status-overlay'; @@ -22,5 +22,5 @@ export const statusModule = new FeatureModule((bind, unbind, isBound, rebind) => const context = { bind, unbind, isBound, rebind }; bindAsService(context, TYPES.IUIExtension, StatusOverlay); bind(TYPES.IDiagramStartup).toService(StatusOverlay); - configureActionHandler(context, ServerStatusAction.KIND, StatusOverlay); + configureActionHandler(context, StatusAction.KIND, StatusOverlay); }); diff --git a/packages/client/src/features/status/status-overlay.ts b/packages/client/src/features/status/status-overlay.ts index 14f8bcc8..3bac805f 100644 --- a/packages/client/src/features/status/status-overlay.ts +++ b/packages/client/src/features/status/status-overlay.ts @@ -14,13 +14,13 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { inject, injectable } from 'inversify'; -import { AbstractUIExtension, IActionHandler, ServerStatusAction, codiconCSSClasses } from '~glsp-sprotty'; +import { AbstractUIExtension, IActionHandler, StatusAction, codiconCSSClasses } from '~glsp-sprotty'; import { GLSPActionDispatcher } from '../../base/action-dispatcher'; import { EditorContextService } from '../../base/editor-context-service'; import { IDiagramStartup } from '../../base/model/diagram-loader'; /** - * A reusable status overlay for rendering (icon + message) and handling of {@link ServerStatusAction}'s. + * A reusable status overlay for rendering (icon + message) and handling of {@link StatusAction}'s. */ @injectable() export class StatusOverlay extends AbstractUIExtension implements IActionHandler, IDiagramStartup { @@ -52,7 +52,7 @@ export class StatusOverlay extends AbstractUIExtension implements IActionHandler containerElement.appendChild(this.statusMessageDiv); } - protected setStatus(status: ServerStatusAction): void { + protected setStatus(status: StatusAction): void { if (this.statusMessageDiv) { this.statusMessageDiv.textContent = status.message; this.removeClasses(this.statusMessageDiv, 1); @@ -80,7 +80,7 @@ export class StatusOverlay extends AbstractUIExtension implements IActionHandler } protected clearStatus(): void { - this.setStatus(ServerStatusAction.create('', { severity: 'NONE' })); + this.setStatus(StatusAction.create('', { severity: 'NONE' })); } protected clearTimeout(): void { @@ -100,7 +100,7 @@ export class StatusOverlay extends AbstractUIExtension implements IActionHandler } } - handle(action: ServerStatusAction): void { + handle(action: StatusAction): void { this.clearTimeout(); if (action.severity === 'NONE') { this.clearStatus(); diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index a2ddd95c..768b68bd 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -16,6 +16,7 @@ // ------------------ Base ------------------ export * from './base/action-dispatcher'; +export * from './base/action-handler-registry'; export * from './base/argumentable'; export * from './base/auto-complete/auto-complete-actions'; export * from './base/auto-complete/auto-complete-widget'; @@ -28,9 +29,9 @@ export * from './base/feedback/feedback-command'; export * from './base/feedback/update-model-command'; export * from './base/focus/focus-state-change-action'; export * from './base/focus/focus-tracker'; -export * from './base/model-initialization-constraint'; export * from './base/model/diagram-loader'; export * from './base/model/glsp-model-source'; +export * from './base/model/model-initialization-constraint'; export * from './base/model/model-registry'; export * from './base/ranked'; export * from './base/selection-clearing-mouse-listener'; @@ -44,14 +45,14 @@ export * from './base/view/view-registry'; // ------------------ Features ------------------ export * from './base/feedback/css-feedback'; export * from './base/view/mouse-tool'; +export * from './features/accessibility/element-navigation/diagram-navigation-tool'; +export * from './features/accessibility/focus-tracker/focus-tracker-tool'; export * from './features/accessibility/key-shortcut/accessible-key-shortcut-tool'; export * from './features/accessibility/resize-key-tool/resize-key-tool'; +export * from './features/accessibility/toast/toast-tool'; export * from './features/accessibility/view-key-tools/deselect-key-tool'; export * from './features/accessibility/view-key-tools/movement-key-tool'; export * from './features/accessibility/view-key-tools/zoom-key-tool'; -export * from './features/accessibility/element-navigation/diagram-navigation-tool'; -export * from './features/accessibility/focus-tracker/focus-tracker-tool'; -export * from './features/accessibility/toast/toast-tool'; export * from './features/bounds/freeform-layout'; export * from './features/bounds/glsp-hidden-bounds-updater'; export * from './features/bounds/hbox-layout'; @@ -125,13 +126,13 @@ export * from './views'; // ------------------ DI Modules ------------------ export * from './base/default.module'; export * from './features/accessibility/accessibility-module'; +export * from './features/accessibility/element-navigation/element-navigation-module'; +export * from './features/accessibility/focus-tracker/focus-tracker-module'; export * from './features/accessibility/move-zoom/move-zoom-module'; export * from './features/accessibility/resize-key-tool/resize-key-module'; export * from './features/accessibility/search/search-palette-module'; -export * from './features/accessibility/view-key-tools/view-key-tools-module'; -export * from './features/accessibility/element-navigation/element-navigation-module'; -export * from './features/accessibility/focus-tracker/focus-tracker-module'; export * from './features/accessibility/toast/toast-module'; +export * from './features/accessibility/view-key-tools/view-key-tools-module'; export * from './features/command-palette/command-palette-module'; export * from './features/context-menu/context-menu-module'; export * from './features/copy-paste/copy-paste-modules'; diff --git a/packages/client/src/standalone-modules.ts b/packages/client/src/standalone-modules.ts index 326b0961..d5e4f657 100644 --- a/packages/client/src/standalone-modules.ts +++ b/packages/client/src/standalone-modules.ts @@ -23,7 +23,7 @@ import { ICommand, ILogger, ModuleConfiguration, - ServerMessageAction, + MessageAction, StartProgressAction, TYPES, UpdateProgressAction, @@ -40,7 +40,7 @@ import { standaloneViewportModule } from './features/viewport/viewport-modules'; export const standaloneFallbackModule = new FeatureModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; bind(FallbackActionHandler).toSelf().inSingletonScope(); - configureActionHandler(context, ServerMessageAction.KIND, FallbackActionHandler); + configureActionHandler(context, MessageAction.KIND, FallbackActionHandler); configureActionHandler(context, StartProgressAction.KIND, FallbackActionHandler); configureActionHandler(context, UpdateProgressAction.KIND, FallbackActionHandler); configureActionHandler(context, EndProgressAction.KIND, FallbackActionHandler); diff --git a/packages/protocol/src/action-protocol/base-protocol.ts b/packages/protocol/src/action-protocol/base-protocol.ts index 528a581e..9e1d8bdc 100644 --- a/packages/protocol/src/action-protocol/base-protocol.ts +++ b/packages/protocol/src/action-protocol/base-protocol.ts @@ -13,7 +13,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { JsonPrimitive } from 'sprotty-protocol'; import * as sprotty from 'sprotty-protocol/lib/actions'; import { AnyObject, hasArrayProp, hasStringProp, TypeGuard } from '../utils/type-util'; @@ -150,7 +149,7 @@ export interface RejectAction extends ResponseAction, sprotty.RejectAction { /** * Optional additional details. */ - detail?: JsonPrimitive; + detail?: string; } export namespace RejectAction { @@ -160,7 +159,7 @@ export namespace RejectAction { return Action.hasKind(object, RejectAction.KIND) && hasStringProp(object, 'message'); } - export function create(message: string, options: { detail?: JsonPrimitive; responseId?: string } = {}): RejectAction { + export function create(message: string, options: { detail?: string; responseId?: string } = {}): RejectAction { return { kind: KIND, responseId: '', diff --git a/packages/protocol/src/action-protocol/client-notification.spec.ts b/packages/protocol/src/action-protocol/client-notification.spec.ts index bfd0bbfb..48480fd8 100644 --- a/packages/protocol/src/action-protocol/client-notification.spec.ts +++ b/packages/protocol/src/action-protocol/client-notification.spec.ts @@ -15,80 +15,74 @@ ********************************************************************************/ /* eslint-disable max-len */ import { expect } from 'chai'; -import { - EndProgressAction, - ServerMessageAction, - ServerStatusAction, - StartProgressAction, - UpdateProgressAction -} from './client-notification'; +import { EndProgressAction, MessageAction, StartProgressAction, StatusAction, UpdateProgressAction } from './client-notification'; /** * Tests for the utility functions declared in the namespaces of the protocol * action definitions. */ describe('Client notification actions', () => { - describe('ServerStatusAction', () => { + describe('StatusAction', () => { describe('is', () => { it('should return true for an object having the correct type and a value for all required interface properties', () => { - const statusAction: ServerStatusAction = { kind: 'serverStatus', message: 'Some', severity: 'INFO' }; - expect(ServerStatusAction.is(statusAction)).to.be.true; + const statusAction: StatusAction = { kind: StatusAction.KIND, message: 'Some', severity: 'INFO' }; + expect(StatusAction.is(statusAction)).to.be.true; }); it('should return false for `undefined`', () => { - expect(ServerStatusAction.is(undefined)).to.be.false; + expect(StatusAction.is(undefined)).to.be.false; }); it('should return false for an object that does not have all required interface properties', () => { - expect(ServerStatusAction.is({ kind: 'notTheRightOne' })).to.be.false; + expect(StatusAction.is({ kind: 'notTheRightOne' })).to.be.false; }); }); describe('create', () => { it('should return an object conforming to the interface with matching properties for the given required arguments and default values for the optional arguments', () => { const message = 'someMessage'; - const expected: ServerStatusAction = { kind: ServerStatusAction.KIND, message, severity: 'INFO' }; - expect(ServerStatusAction.create(message)).to.deep.equals(expected); + const expected: StatusAction = { kind: StatusAction.KIND, message, severity: 'INFO' }; + expect(StatusAction.create(message)).to.deep.equals(expected); }); it('should return an object conforming to the interface with matching properties for the given required and optional arguments', () => { - const expected: ServerStatusAction = { - kind: ServerStatusAction.KIND, + const expected: StatusAction = { + kind: StatusAction.KIND, message: 'someMessage', severity: 'ERROR', timeout: 5 }; const { message, severity, timeout } = expected; - expect(ServerStatusAction.create(message, { severity, timeout })).to.deep.equals(expected); + expect(StatusAction.create(message, { severity, timeout })).to.deep.equals(expected); }); }); }); - describe('ServerMessageAction', () => { + describe('MessageAction', () => { describe('is', () => { it('should return true for an object having the correct type and a value for all required interface properties', () => { - const messageAction: ServerMessageAction = { kind: 'serverMessage', message: '', severity: 'INFO' }; - expect(ServerMessageAction.is(messageAction)).to.be.true; + const messageAction: MessageAction = { kind: MessageAction.KIND, message: '', severity: 'INFO' }; + expect(MessageAction.is(messageAction)).to.be.true; }); it('should return false for `undefined`', () => { - expect(ServerMessageAction.is(undefined)).to.be.false; + expect(MessageAction.is(undefined)).to.be.false; }); it('should return false for an object that does not have all required interface properties', () => { - expect(ServerMessageAction.is({ kind: 'notTheRightOne' })).to.be.false; + expect(MessageAction.is({ kind: 'notTheRightOne' })).to.be.false; }); }); describe('create', () => { it('should return an object conforming to the interface with matching properties for the given required arguments and default values for the optional arguments', () => { const message = 'someMessage'; - const expected: ServerMessageAction = { kind: ServerMessageAction.KIND, message, severity: 'INFO' }; - expect(ServerMessageAction.create(message)).to.deep.equals(expected); + const expected: MessageAction = { kind: MessageAction.KIND, message, severity: 'INFO' }; + expect(MessageAction.create(message)).to.deep.equals(expected); }); it('should return an object conforming to the interface with matching properties for the given required and optional arguments', () => { - const expected: ServerMessageAction = { - kind: ServerMessageAction.KIND, + const expected: MessageAction = { + kind: MessageAction.KIND, message: 'someMessage', details: 'details', severity: 'ERROR' }; const { message, severity, details } = expected; - expect(ServerMessageAction.create(message, { severity, details })).to.deep.equals(expected); + expect(MessageAction.create(message, { severity, details })).to.deep.equals(expected); }); }); }); diff --git a/packages/protocol/src/action-protocol/client-notification.ts b/packages/protocol/src/action-protocol/client-notification.ts index 13efc41a..e9106c22 100644 --- a/packages/protocol/src/action-protocol/client-notification.ts +++ b/packages/protocol/src/action-protocol/client-notification.ts @@ -17,17 +17,17 @@ import { hasStringProp } from '../utils/type-util'; import { Action } from './base-protocol'; /** - * Sent by the server to signal a state change. + * Sent by the server (or the client) to signal a status change. * If a timeout is given the respective status should disappear after the timeout is reached. * The corresponding namespace declares the action kind as constant and offers helper functions for type guard checks - * and creating new `ServerStatusActions`. + * and creating new `StatusAction`s. */ -export interface ServerStatusAction extends Action { - kind: typeof ServerStatusAction.KIND; +export interface StatusAction extends Action { + kind: typeof StatusAction.KIND; /** * The severity of the status. */ - severity: ServerSeverity; + severity: SeverityLevel; /** * The user-facing message describing the status. @@ -40,14 +40,14 @@ export interface ServerStatusAction extends Action { timeout?: number; } -export namespace ServerStatusAction { - export const KIND = 'serverStatus'; +export namespace StatusAction { + export const KIND = 'status'; - export function is(object: any): object is ServerStatusAction { + export function is(object: any): object is StatusAction { return Action.hasKind(object, KIND) && hasStringProp(object, 'severity') && hasStringProp(object, 'message'); } - export function create(message: string, options: { severity?: ServerSeverity; timeout?: number } = {}): ServerStatusAction { + export function create(message: string, options: { severity?: SeverityLevel; timeout?: number } = {}): StatusAction { return { kind: KIND, severity: 'INFO', @@ -61,19 +61,19 @@ export namespace ServerStatusAction { * The possible server status severity levels. */ -export type ServerSeverity = 'NONE' | 'INFO' | 'WARNING' | 'ERROR' | 'FATAL' | 'OK'; +export type SeverityLevel = 'NONE' | 'INFO' | 'WARNING' | 'ERROR' | 'FATAL' | 'OK'; /** - * Sent by the server to notify the user about something of interest. Typically this message is handled by + * Sent by the server (or the client) to notify the user about something of interest. Typically this message is handled by * the client by showing a message to the user with the application's message service. * If a timeout is given the respective message should disappear after the timeout is reached. * The corresponding namespace declares the action kind as constant and offers helper functions for type guard checks - * and creating new `ServerMessageActions`. + * and creating new `MessageAction`s. */ -export interface ServerMessageAction extends Action { - kind: typeof ServerMessageAction.KIND; +export interface MessageAction extends Action { + kind: typeof MessageAction.KIND; - severity: ServerSeverity; + severity: SeverityLevel; /** * The message that shall be shown to the user. @@ -86,20 +86,20 @@ export interface ServerMessageAction extends Action { details?: string; } -export namespace ServerMessageAction { - export const KIND = 'serverMessage'; +export namespace MessageAction { + export const KIND = 'message'; - export function is(object: any): object is ServerMessageAction { + export function is(object: any): object is MessageAction { return Action.hasKind(object, KIND) && hasStringProp(object, 'message') && hasStringProp(object, 'severity'); } export function create( message: string, options: { - severity?: ServerSeverity; + severity?: SeverityLevel; details?: string; } = {} - ): ServerMessageAction { + ): MessageAction { return { kind: KIND, message, @@ -110,7 +110,7 @@ export namespace ServerMessageAction { } /** - * Sent by the server to the client to request presenting the progress of a long running process in the UI. + * Sent to request presenting the progress of a long running process in the UI. */ export interface StartProgressAction extends Action { kind: typeof StartProgressAction.KIND; @@ -149,7 +149,7 @@ export namespace StartProgressAction { } /** - * Sent by the server to the client to presenting an update of the progress of a long running process in the UI. + * Sent to presenting an update of the progress of a long running process in the UI. */ export interface UpdateProgressAction extends Action { kind: typeof UpdateProgressAction.KIND; @@ -191,7 +191,7 @@ export namespace UpdateProgressAction { } /** - * Sent by the server to the client to end the reporting of a progress. + * Sent to end the reporting of a progress. */ export interface EndProgressAction extends Action { kind: typeof EndProgressAction.KIND; diff --git a/packages/protocol/src/client-server-protocol/base-glsp-client.spec.ts b/packages/protocol/src/client-server-protocol/base-glsp-client.spec.ts index eadf3056..298b4590 100644 --- a/packages/protocol/src/client-server-protocol/base-glsp-client.spec.ts +++ b/packages/protocol/src/client-server-protocol/base-glsp-client.spec.ts @@ -149,18 +149,18 @@ describe('Node GLSP Client', () => { describe('initializeClientSession', () => { it('should fail if server is not configured', async () => { resetClient(false); - await expectToThrowAsync(() => client.initializeClientSession({ clientSessionId: '', diagramType: '' })); + await expectToThrowAsync(() => client.initializeClientSession({ clientSessionId: '', diagramType: '', clientActionKinds: [] })); expect(server.initializeClientSession.called).to.be.false; }); it('should fail if client is not running', async () => { resetClient(false); client.configureServer(server); - await expectToThrowAsync(() => client.initializeClientSession({ clientSessionId: '', diagramType: '' })); + await expectToThrowAsync(() => client.initializeClientSession({ clientSessionId: '', diagramType: '', clientActionKinds: [] })); expect(server.initializeClientSession.called).to.be.false; }); it('should invoke the corresponding server method', async () => { resetClient(); - const result = await client.initializeClientSession({ clientSessionId: '', diagramType: '' }); + const result = await client.initializeClientSession({ clientSessionId: '', diagramType: '', clientActionKinds: [] }); expect(result).to.be.undefined; expect(server.initializeClientSession.calledOnce).to.be.true; }); diff --git a/packages/protocol/src/client-server-protocol/jsonrpc/base-jsonrpc-glsp-client.spec.ts b/packages/protocol/src/client-server-protocol/jsonrpc/base-jsonrpc-glsp-client.spec.ts index 64a32b41..d9c57418 100644 --- a/packages/protocol/src/client-server-protocol/jsonrpc/base-jsonrpc-glsp-client.spec.ts +++ b/packages/protocol/src/client-server-protocol/jsonrpc/base-jsonrpc-glsp-client.spec.ts @@ -158,12 +158,12 @@ describe('Base JSON-RPC GLSP Client', () => { describe('initializeClientSession', () => { it('should fail if client is not running', async () => { await resetClient(false); - await expectToThrowAsync(() => client.initializeClientSession({ clientSessionId: '', diagramType: '' })); + await expectToThrowAsync(() => client.initializeClientSession({ clientSessionId: '', diagramType: '', clientActionKinds: [] })); expect(connection.sendRequest.called).to.be.false; }); it('should invoke the corresponding server method', async () => { await resetClient(); - const params = { clientSessionId: '', diagramType: '' }; + const params = { clientSessionId: '', diagramType: '', clientActionKinds: [] }; const initializeSessionMock = connection.sendRequest.withArgs(JsonrpcGLSPClient.InitializeClientSessionRequest, params); const result = await client.initializeClientSession(params); expect(result).to.be.undefined; diff --git a/packages/protocol/src/client-server-protocol/types.ts b/packages/protocol/src/client-server-protocol/types.ts index 44287d70..862c0895 100644 --- a/packages/protocol/src/client-server-protocol/types.ts +++ b/packages/protocol/src/client-server-protocol/types.ts @@ -66,6 +66,12 @@ export interface InitializeClientSessionParameters { */ diagramType: string; + /** + * The set of action kinds that can be handled by the client. + * Used by the server to know which dispatched actions should be forwarded to the client. + */ + clientActionKinds: string[]; + /** * Additional custom arguments. */