Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/client/src/base/action-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { RequestAction } from '@eclipse-glsp/protocol';
import {
Action,
ActionDispatcher,
Expand All @@ -23,7 +24,6 @@ import {
GModelRoot,
IActionDispatcher,
RejectAction,
RequestAction,
ResponseAction,
SetModelAction,
TYPES
Expand Down Expand Up @@ -195,7 +195,7 @@ export class GLSPActionDispatcher extends ActionDispatcher implements IGModelRoo
}

override request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res> {
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();
}
Expand All @@ -214,13 +214,16 @@ export class GLSPActionDispatcher extends ActionDispatcher implements IGModelRoo
*/
requestUntil<Res extends ResponseAction>(
action: RequestAction<Res>,
timeoutMs = 2000,
timeoutMs: number = action.timeout ?? 2000,
rejectOnTimeout = false
): Promise<Res | undefined> {
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(() => {
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/base/default.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { GetEditorContextAction } from '@eclipse-glsp/protocol';
import {
ActionHandlerRegistry,
FeatureModule,
Expand Down Expand Up @@ -83,6 +84,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);
Expand Down
15 changes: 14 additions & 1 deletion packages/client/src/base/editor-context-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { GetEditorContextAction, SetEditorContextAction } from '@eclipse-glsp/protocol';
import {
Action,
Args,
Expand Down Expand Up @@ -189,14 +190,26 @@ export class EditorContextService implements IActionHandler, Disposable, IDiagra
};
}

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): SetEditorContextAction {
return SetEditorContextAction.create({
selectedElementIds: this.selectionService.getSelectedElementIDs(),
lastMousePosition: this.mousePositionTracker.lastPositionOnDiagram,
viewport: this.viewportData,
canvasBounds: this.canvasBounds,
responseId: action.requestId
});
}

protected handleSetEditModeAction(action: SetEditModeAction): void {
const oldValue = this._editMode;
this._editMode = action.editMode;
Expand Down
29 changes: 29 additions & 0 deletions packages/client/src/base/model/glsp-model-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { RequestAction } from '@eclipse-glsp/protocol';
import {
Action,
ActionMessage,
Expand All @@ -26,9 +27,12 @@ import {
InitializeClientSessionParameters,
InitializeResult,
ModelSource,
RejectAction,
ResponseAction,
TYPES
} from '@eclipse-glsp/sprotty';
import { inject, injectable, preDestroy } from 'inversify';
import { GLSPActionDispatcher } from '../action-dispatcher';
import { GLSPActionHandlerRegistry } from '../action-handler-registry';
import { IDiagramOptions } from './diagram-loader';

Expand Down Expand Up @@ -94,6 +98,8 @@ export class GLSPModelSource extends ModelSource implements Disposable {
@inject(TYPES.IDiagramOptions)
protected options: IDiagramOptions;

declare readonly actionDispatcher: GLSPActionDispatcher;
Comment thread
tortmayr marked this conversation as resolved.
Outdated

protected toDispose = new DisposableCollection();
clientId: string;

Expand Down Expand Up @@ -164,9 +170,32 @@ 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<ResponseAction>): Promise<void> {
try {
const response = action.timeout !== undefined
? await this.actionDispatcher.requestUntil(action, action.timeout, true)
: await this.actionDispatcher.request(action);
if (response) {
Comment thread
tortmayr marked this conversation as resolved.
this.forwardToServer(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.forwardToServer(reject);
}
}

override initialize(registry: GLSPActionHandlerRegistry): void {
// Registering actions here is discouraged and it's recommended
// to implemented dedicated action handlers.
Expand Down
30 changes: 30 additions & 0 deletions packages/client/src/features/select/get-selection-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/********************************************************************************
* Copyright (c) 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
* 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 { Action, GetSelectionAction, IActionHandler, SelectionResult } from '@eclipse-glsp/sprotty';
import { inject, injectable } from 'inversify';
import { SelectionService } from '../../base/selection-service';

@injectable()
Comment thread
tortmayr marked this conversation as resolved.
Outdated
export class GetSelectionHandler implements IActionHandler {
@inject(SelectionService)
protected selectionService: SelectionService;

handle(action: Action): Action | void {
if (GetSelectionAction.is(action)) {
return SelectionResult.create(this.selectionService.getSelectedElementIDs(), action.requestId);
}
}
}
12 changes: 11 additions & 1 deletion packages/client/src/features/select/select-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { FeatureModule, SelectKeyboardListener, TYPES, bindAsService, configureCommand } from '@eclipse-glsp/sprotty';
import {
FeatureModule,
GetSelectionAction,
SelectKeyboardListener,
TYPES,
bindAsService,
configureActionHandler,
configureCommand
} from '@eclipse-glsp/sprotty';
import { SelectAllCommand, SelectCommand } from '../../base/selection-service';
import { GetSelectionHandler } from './get-selection-handler';
import { SelectFeedbackCommand } from './select-feedback-command';
import { RankedSelectMouseListener } from './select-mouse-listener';

Expand All @@ -24,6 +33,7 @@ export const selectModule = new FeatureModule(
configureCommand(context, SelectCommand);
configureCommand(context, SelectAllCommand);
configureCommand(context, SelectFeedbackCommand);
configureActionHandler(context, GetSelectionAction.KIND, GetSelectionHandler);
Comment thread
tortmayr marked this conversation as resolved.
Outdated
bindAsService(context, TYPES.MouseListener, RankedSelectMouseListener);
},
{ featureId: Symbol('select') }
Expand Down
6 changes: 6 additions & 0 deletions packages/protocol/src/action-protocol/base-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ export interface RequestAction<Res extends ResponseAction> 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
*/
Expand Down
96 changes: 96 additions & 0 deletions packages/protocol/src/action-protocol/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { Bounds, Point, Viewport } from '../re-exports';
import { hasArrayProp, hasObjectProp, hasStringProp } from '../utils/type-util';
import { Action, RequestAction, ResponseAction } from './base-protocol';
import { Args, EditorContext, LabeledAction } from './types';
Expand Down Expand Up @@ -85,3 +86,98 @@ 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 {@link GetSelectionAction} (sprotty built-in).
* - For viewport and canvas bounds only, use {@link GetViewportAction} (sprotty built-in).
*/
export interface GetEditorContextAction extends RequestAction<SetEditorContextAction> {
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 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 SetEditorContextAction extends ResponseAction {
Comment thread
tortmayr marked this conversation as resolved.
Outdated
kind: typeof SetEditorContextAction.KIND;

/**
* The list of currently selected element identifiers.
* For a dedicated selection query, use {@link GetSelectionAction} instead.
*/
readonly selectedElementIds: string[];
Comment thread
tortmayr marked this conversation as resolved.
Outdated

/**
* The last recorded mouse position on the diagram, or `undefined` if no position has been recorded.
*/
readonly lastMousePosition?: Point;

/**
* The current viewport (scroll position and zoom level).
* For a dedicated viewport query, use {@link GetViewportAction} instead.
*/
readonly viewport: Viewport;

/**
* The bounds of the canvas element in the browser.
* For a dedicated viewport and canvas bounds query, use {@link GetViewportAction} instead.
*/
readonly canvasBounds: Bounds;

/**
* Custom arguments for application-specific client state.
*/
readonly args?: Args;
}

export namespace SetEditorContextAction {
export const KIND = 'setEditorContext';

export function is(object: unknown): object is SetEditorContextAction {
return Action.hasKind(object, KIND) && hasArrayProp(object, 'selectedElementIds');
}

export function create(
options: {
selectedElementIds: string[];
lastMousePosition?: Point;
viewport: Viewport;
canvasBounds: Bounds;
args?: Args;
responseId?: string;
}
): SetEditorContextAction {
return {
kind: KIND,
responseId: '',
...options
};
}
}
Loading