diff --git a/packages/client/src/base/action-dispatcher.ts b/packages/client/src/base/action-dispatcher.ts index a2ba69a2..f6e7bb65 100644 --- a/packages/client/src/base/action-dispatcher.ts +++ b/packages/client/src/base/action-dispatcher.ts @@ -195,7 +195,7 @@ export class GLSPActionDispatcher extends ActionDispatcher implements IGModelRoo } override request(action: RequestAction): Promise { - if (!action.requestId && action.requestId === '') { + if (!action.requestId || action.requestId === '') { // No request id has been specified. So we use a generated one. action.requestId = RequestAction.generateRequestId(); } @@ -214,13 +214,16 @@ export class GLSPActionDispatcher extends ActionDispatcher implements IGModelRoo */ requestUntil( action: RequestAction, - timeoutMs = 2000, + timeoutMs: number = action.timeout ?? 2000, rejectOnTimeout = false ): Promise { - if (!action.requestId && action.requestId === '') { + if (!action.requestId || action.requestId === '') { // No request id has been specified. So we use a generated one. action.requestId = RequestAction.generateRequestId(); } + // Stamp the effective timeout onto the action so the receiving side + // (handleServerRequest/handleClientRequest) can respect it. + action.timeout = timeoutMs; const requestId = action.requestId; const timeout = setTimeout(() => { diff --git a/packages/client/src/base/default.module.ts b/packages/client/src/base/default.module.ts index b1dac6a2..b9c3231a 100644 --- a/packages/client/src/base/default.module.ts +++ b/packages/client/src/base/default.module.ts @@ -16,6 +16,8 @@ import { ActionHandlerRegistry, FeatureModule, + GetEditorContextAction, + GetSelectionAction, KeyTool, LocationPostprocessor, MousePositionTracker, @@ -83,6 +85,7 @@ export const defaultModule = new FeatureModule( configureActionHandler(context, SetEditModeAction.KIND, EditorContextService); configureActionHandler(context, SetDirtyStateAction.KIND, EditorContextService); + configureActionHandler(context, GetEditorContextAction.KIND, EditorContextService); bind(FocusTracker).toSelf().inSingletonScope(); bind(TYPES.IDiagramStartup).toService(FocusTracker); @@ -121,6 +124,7 @@ export const defaultModule = new FeatureModule( bind(SelectionService).toSelf().inSingletonScope(); bind(TYPES.IGModelRootListener).toService(SelectionService); bind(TYPES.IDiagramStartup).toService(SelectionService); + configureActionHandler(context, GetSelectionAction.KIND, SelectionService); // Feedback Support ------------------------------------ // Generic re-usable feedback modifying css classes diff --git a/packages/client/src/base/editor-context-service.ts b/packages/client/src/base/editor-context-service.ts index b7b97e6a..ac81bfb3 100644 --- a/packages/client/src/base/editor-context-service.ts +++ b/packages/client/src/base/editor-context-service.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2020-2025 EclipseSource and others. + * Copyright (c) 2020-2026 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 @@ -21,14 +21,14 @@ import { DisposableCollection, EditMode, EditorContext, + EditorContextResult, Emitter, Event, - findParentByFeature, GModelElement, GModelRoot, + GetEditorContextAction, IActionDispatcher, IActionHandler, - isViewport, LazyInjector, MaybePromise, MousePositionTracker, @@ -37,7 +37,9 @@ import { SetEditModeAction, TYPES, ValueChange, - Viewport + Viewport, + findParentByFeature, + isViewport } from '@eclipse-glsp/sprotty'; import { inject, injectable, postConstruct, preDestroy } from 'inversify'; import { FocusChange, FocusTracker } from './focus/focus-tracker'; @@ -177,6 +179,8 @@ export class EditorContextService implements IActionHandler, Disposable, IDiagra return { selectedElementIds: Array.from(this.selectionService.getSelectedElementIDs()), lastMousePosition: this.mousePositionTracker.lastPositionOnDiagram, + viewport: this.viewportData, + canvasBounds: this.canvasBounds, args }; } @@ -185,18 +189,26 @@ export class EditorContextService implements IActionHandler, Disposable, IDiagra return { selectedElementIds, lastMousePosition: this.mousePositionTracker.lastPositionOnDiagram, + viewport: this.viewportData, + canvasBounds: this.canvasBounds, args }; } - handle(action: Action): void { + handle(action: Action): Action | void { if (SetEditModeAction.is(action)) { this.handleSetEditModeAction(action); } else if (SetDirtyStateAction.is(action)) { this.handleSetDirtyStateAction(action); + } else if (GetEditorContextAction.is(action)) { + return this.handleGetEditorContext(action); } } + protected handleGetEditorContext(action: GetEditorContextAction): EditorContextResult { + return EditorContextResult.create(this.get(), { responseId: action.requestId }); + } + protected handleSetEditModeAction(action: SetEditModeAction): void { const oldValue = this._editMode; this._editMode = action.editMode; diff --git a/packages/client/src/base/model/glsp-model-source.ts b/packages/client/src/base/model/glsp-model-source.ts index 1eae4f71..4a2843fa 100644 --- a/packages/client/src/base/model/glsp-model-source.ts +++ b/packages/client/src/base/model/glsp-model-source.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2023-2024 EclipseSource and others. + * Copyright (c) 2023-2026 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 @@ -22,10 +22,14 @@ import { DisposeClientSessionParameters, GLSPClient, GModelRootSchema, + IActionDispatcher, ILogger, InitializeClientSessionParameters, InitializeResult, ModelSource, + RejectAction, + RequestAction, + ResponseAction, TYPES } from '@eclipse-glsp/sprotty'; import { inject, injectable, preDestroy } from 'inversify'; @@ -94,6 +98,8 @@ export class GLSPModelSource extends ModelSource implements Disposable { @inject(TYPES.IDiagramOptions) protected options: IDiagramOptions; + declare readonly actionDispatcher: IActionDispatcher; + protected toDispose = new DisposableCollection(); clientId: string; @@ -164,9 +170,37 @@ export class GLSPModelSource extends ModelSource implements Disposable { const action = message.action; ServerAction.mark(action); this.logger.log(this, 'receiving', action); + if (RequestAction.is(action)) { + this.handleServerRequest(action); + return; + } this.actionDispatcher.dispatch(action); } + // Fire-and-forget: intentionally not awaited by messageReceived() + protected async handleServerRequest(action: RequestAction): Promise { + try { + const response = + action.timeout !== undefined + ? await this.actionDispatcher.requestUntil(action, action.timeout, true) + : await this.actionDispatcher.request(action); + if (response) { + this.sendResponseToServer(response); + } + } catch (error) { + this.logger.error(this, `Failed to handle server request '${action.kind}' (${action.requestId})`, error); + const reject = RejectAction.create(`Failed to handle request '${action.kind}' (${action.requestId})`, { + responseId: action.requestId, + detail: error?.toString?.() + }); + this.sendResponseToServer(reject); + } + } + + protected sendResponseToServer(response: ResponseAction): void { + this.forwardToServer(response); + } + override initialize(registry: GLSPActionHandlerRegistry): void { // Registering actions here is discouraged and it's recommended // to implemented dedicated action handlers. diff --git a/packages/client/src/base/selection-service.ts b/packages/client/src/base/selection-service.ts index 69ee09b5..700e99a5 100644 --- a/packages/client/src/base/selection-service.ts +++ b/packages/client/src/base/selection-service.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2019-2024 EclipseSource and others. + * Copyright (c) 2019-2026 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 @@ -23,12 +23,15 @@ import { Emitter, Event, GChildElement, + GetSelectionAction, GModelElement, GModelRoot, + IActionHandler, ILogger, LazyInjector, SelectAction, SelectAllAction, + SelectionResult, SprottySelectAllCommand, SprottySelectCommand, TYPES, @@ -60,7 +63,7 @@ export interface SelectionChange { } @injectable() -export class SelectionService implements IGModelRootListener, Disposable, IDiagramStartup { +export class SelectionService implements IGModelRootListener, IActionHandler, Disposable, IDiagramStartup { @inject(TYPES.IFeedbackActionDispatcher) protected feedbackDispatcher: IFeedbackActionDispatcher; @@ -84,6 +87,12 @@ export class SelectionService implements IGModelRootListener, Disposable, IDiagr this.lazyInjector.getAll(TYPES.ISelectionListener).forEach(listener => this.addListener(listener)); } + handle(action: Action): Action | void { + if (GetSelectionAction.is(action)) { + return SelectionResult.create(this.getSelectedElementIDs(), action.requestId); + } + } + @preDestroy() dispose(): void { this.toDispose.dispose(); diff --git a/packages/client/src/features/label-edit-ui/label-edit-ui.ts b/packages/client/src/features/label-edit-ui/label-edit-ui.ts index 096ca4ca..4bf30187 100644 --- a/packages/client/src/features/label-edit-ui/label-edit-ui.ts +++ b/packages/client/src/features/label-edit-ui/label-edit-ui.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2024 EclipseSource and others. + * Copyright (c) 2024-2026 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 diff --git a/packages/client/src/features/select/select-module.ts b/packages/client/src/features/select/select-module.ts index b121e64f..b8c52c45 100644 --- a/packages/client/src/features/select/select-module.ts +++ b/packages/client/src/features/select/select-module.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2019-2024 EclipseSource and others. + * Copyright (c) 2019-2026 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 diff --git a/packages/protocol/src/action-protocol/base-protocol.ts b/packages/protocol/src/action-protocol/base-protocol.ts index c6643dfc..bbfe7955 100644 --- a/packages/protocol/src/action-protocol/base-protocol.ts +++ b/packages/protocol/src/action-protocol/base-protocol.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2021-2023 STMicroelectronics and others. + * Copyright (c) 2021-2026 STMicroelectronics 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 @@ -82,6 +82,12 @@ export interface RequestAction extends Action, sprot * Unique id for this request. In order to match a response to this request, the response needs to have the same id. */ requestId: string; + /** + * Optional timeout in milliseconds. When set, `requestUntil()` uses this value instead of its + * default timeout. This allows the sender to control how long the receiver waits for a response. + * Precedence: explicit `timeoutMs` parameter > `action.timeout` > default (2000ms). + */ + timeout?: number; /** * Used to ensure correct typing. Clients must not use this property */ diff --git a/packages/protocol/src/action-protocol/contexts.ts b/packages/protocol/src/action-protocol/contexts.ts index 0b68e55d..96a9c91c 100644 --- a/packages/protocol/src/action-protocol/contexts.ts +++ b/packages/protocol/src/action-protocol/contexts.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2021-2023 STMicroelectronics and others. + * Copyright (c) 2021-2026 STMicroelectronics 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 @@ -85,3 +85,67 @@ export namespace SetContextActions { }; } } + +/** + * Sent from the server to the client to request the current {@link EditorContext}. This is the server-initiated + * counterpart to the `editorContext` parameter that is included in many client-to-server requests. + * + * All fields in the response represent a snapshot of the client state at the time the response is generated. + * There is no guarantee that the state has not changed by the time the server processes the response. + * + * If you only need a subset of the editor context, consider using a more specific action instead: + * - For selected elements only, use `GetSelectionAction`. + * - For viewport and canvas bounds only, use `GetViewportAction`. + */ +export interface GetEditorContextAction extends RequestAction { + kind: typeof GetEditorContextAction.KIND; +} + +export namespace GetEditorContextAction { + export const KIND = 'getEditorContext'; + + export function is(object: unknown): object is GetEditorContextAction { + return RequestAction.hasKind(object, KIND); + } + + export function create(options: { requestId?: string; timeout?: number } = {}): GetEditorContextAction { + return { + kind: KIND, + requestId: '', + ...options + }; + } +} + +/** + * Response to a {@link GetEditorContextAction} containing a snapshot of the client-side editor state. + * + * All fields in the {@link EditorContext} reflect the state at the time of response generation. + * The server should not assume that these values are still current when processing the response, + * as the client state may have changed in the meantime. + */ +export interface EditorContextResult extends ResponseAction { + kind: typeof EditorContextResult.KIND; + + /** + * The editor context snapshot. + */ + readonly editorContext: EditorContext; +} + +export namespace EditorContextResult { + export const KIND = 'editorContextResult'; + + export function is(object: unknown): object is EditorContextResult { + return Action.hasKind(object, KIND) && hasObjectProp(object, 'editorContext'); + } + + export function create(editorContext: EditorContext, options: { responseId?: string } = {}): EditorContextResult { + return { + kind: KIND, + responseId: '', + editorContext, + ...options + }; + } +} diff --git a/packages/protocol/src/action-protocol/types.ts b/packages/protocol/src/action-protocol/types.ts index abfd8ba6..b7551524 100644 --- a/packages/protocol/src/action-protocol/types.ts +++ b/packages/protocol/src/action-protocol/types.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import * as sprotty from 'sprotty-protocol'; -import { Dimension, Point } from 'sprotty-protocol'; +import { Bounds, Dimension, Point, Viewport } from 'sprotty-protocol'; import { GModelElementSchema } from '../model/model-schema'; import { AnyObject, hasArrayProp, hasStringProp } from '../utils/type-util'; import { Action } from './base-protocol'; @@ -121,6 +121,16 @@ export interface EditorContext { */ readonly lastMousePosition?: Point; + /** + * The current viewport (scroll position and zoom level). + */ + readonly viewport?: Viewport; + + /** + * The bounds of the canvas element in the browser. + */ + readonly canvasBounds?: Bounds; + /** * Custom arguments. */