@eclipse-glsp/client 2.7.0-next.9 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/lib/base/action-dispatcher.d.ts +2 -1
  2. package/lib/base/action-dispatcher.d.ts.map +1 -1
  3. package/lib/base/action-dispatcher.js +48 -24
  4. package/lib/base/action-dispatcher.js.map +1 -1
  5. package/lib/base/default.module.d.ts.map +1 -1
  6. package/lib/base/default.module.js +2 -0
  7. package/lib/base/default.module.js.map +1 -1
  8. package/lib/base/editor-context-service.d.ts +4 -3
  9. package/lib/base/editor-context-service.d.ts.map +1 -1
  10. package/lib/base/editor-context-service.js +11 -1
  11. package/lib/base/editor-context-service.js.map +1 -1
  12. package/lib/base/model/diagram-loader.d.ts +8 -6
  13. package/lib/base/model/diagram-loader.d.ts.map +1 -1
  14. package/lib/base/model/diagram-loader.js +1 -1
  15. package/lib/base/model/diagram-loader.js.map +1 -1
  16. package/lib/base/model/glsp-model-source.d.ts +5 -2
  17. package/lib/base/model/glsp-model-source.d.ts.map +1 -1
  18. package/lib/base/model/glsp-model-source.js +28 -1
  19. package/lib/base/model/glsp-model-source.js.map +1 -1
  20. package/lib/base/selection-service.d.ts +4 -3
  21. package/lib/base/selection-service.d.ts.map +1 -1
  22. package/lib/base/selection-service.js +6 -1
  23. package/lib/base/selection-service.js.map +1 -1
  24. package/lib/base/shortcuts/available-shortcuts-extension.d.ts +1 -1
  25. package/lib/base/shortcuts/available-shortcuts-extension.d.ts.map +1 -1
  26. package/lib/base/shortcuts/available-shortcuts-extension.js +3 -4
  27. package/lib/base/shortcuts/available-shortcuts-extension.js.map +1 -1
  28. package/lib/base/view/mouse-tool.d.ts +1 -1
  29. package/lib/base/view/mouse-tool.d.ts.map +1 -1
  30. package/lib/base/view/mouse-tool.js +3 -19
  31. package/lib/base/view/mouse-tool.js.map +1 -1
  32. package/lib/features/change-bounds/move-element-handler.d.ts +2 -3
  33. package/lib/features/change-bounds/move-element-handler.d.ts.map +1 -1
  34. package/lib/features/change-bounds/move-element-handler.js +2 -3
  35. package/lib/features/change-bounds/move-element-handler.js.map +1 -1
  36. package/lib/features/change-bounds/resize/resize-handler.d.ts +2 -3
  37. package/lib/features/change-bounds/resize/resize-handler.d.ts.map +1 -1
  38. package/lib/features/change-bounds/resize/resize-handler.js +2 -3
  39. package/lib/features/change-bounds/resize/resize-handler.js.map +1 -1
  40. package/lib/features/export/default-png-diagram-exporter.d.ts +52 -0
  41. package/lib/features/export/default-png-diagram-exporter.d.ts.map +1 -0
  42. package/lib/features/export/default-png-diagram-exporter.js +157 -0
  43. package/lib/features/export/default-png-diagram-exporter.js.map +1 -0
  44. package/lib/features/export/default-svg-diagram-exporter.d.ts +32 -0
  45. package/lib/features/export/default-svg-diagram-exporter.d.ts.map +1 -0
  46. package/lib/features/export/default-svg-diagram-exporter.js +54 -0
  47. package/lib/features/export/default-svg-diagram-exporter.js.map +1 -0
  48. package/lib/features/export/diagram-export-postprocessor.d.ts +33 -0
  49. package/lib/features/export/diagram-export-postprocessor.d.ts.map +1 -0
  50. package/lib/features/export/diagram-export-postprocessor.js +84 -0
  51. package/lib/features/export/diagram-export-postprocessor.js.map +1 -0
  52. package/lib/features/export/diagram-exporter.d.ts +44 -0
  53. package/lib/features/export/diagram-exporter.d.ts.map +1 -0
  54. package/lib/features/export/diagram-exporter.js +18 -0
  55. package/lib/features/export/diagram-exporter.js.map +1 -0
  56. package/lib/features/export/export-modules.d.ts +1 -1
  57. package/lib/features/export/export-modules.d.ts.map +1 -1
  58. package/lib/features/export/export-modules.js +20 -3
  59. package/lib/features/export/export-modules.js.map +1 -1
  60. package/lib/features/export/export-result-action-handler.d.ts +28 -0
  61. package/lib/features/export/export-result-action-handler.d.ts.map +1 -0
  62. package/lib/features/export/export-result-action-handler.js +72 -0
  63. package/lib/features/export/export-result-action-handler.js.map +1 -0
  64. package/lib/features/export/export-svg-action-handler.d.ts +19 -0
  65. package/lib/features/export/export-svg-action-handler.d.ts.map +1 -1
  66. package/lib/features/export/export-svg-action-handler.js +4 -15
  67. package/lib/features/export/export-svg-action-handler.js.map +1 -1
  68. package/lib/features/export/glsp-svg-exporter.d.ts +18 -1
  69. package/lib/features/export/glsp-svg-exporter.d.ts.map +1 -1
  70. package/lib/features/export/glsp-svg-exporter.js +44 -11
  71. package/lib/features/export/glsp-svg-exporter.js.map +1 -1
  72. package/lib/features/export/request-export-command.d.ts +29 -0
  73. package/lib/features/export/request-export-command.d.ts.map +1 -0
  74. package/lib/features/export/request-export-command.js +73 -0
  75. package/lib/features/export/request-export-command.js.map +1 -0
  76. package/lib/features/export/request-export-key-listener.d.ts +21 -0
  77. package/lib/features/export/request-export-key-listener.d.ts.map +1 -0
  78. package/lib/features/export/request-export-key-listener.js +40 -0
  79. package/lib/features/export/request-export-key-listener.js.map +1 -0
  80. package/lib/features/helper-lines/helper-line-feedback.d.ts +1 -1
  81. package/lib/features/helper-lines/helper-line-feedback.d.ts.map +1 -1
  82. package/lib/features/helper-lines/helper-line-feedback.js +2 -3
  83. package/lib/features/helper-lines/helper-line-feedback.js.map +1 -1
  84. package/lib/features/routing/edge-router.d.ts +2 -1
  85. package/lib/features/routing/edge-router.d.ts.map +1 -1
  86. package/lib/features/routing/edge-router.js +2 -1
  87. package/lib/features/routing/edge-router.js.map +1 -1
  88. package/lib/features/routing/routing-module.d.ts +1 -1
  89. package/lib/features/routing/routing-module.d.ts.map +1 -1
  90. package/lib/features/routing/routing-module.js +7 -1
  91. package/lib/features/routing/routing-module.js.map +1 -1
  92. package/lib/features/routing/sticky-manhattan-anchors.d.ts +29 -0
  93. package/lib/features/routing/sticky-manhattan-anchors.d.ts.map +1 -0
  94. package/lib/features/routing/sticky-manhattan-anchors.js +62 -0
  95. package/lib/features/routing/sticky-manhattan-anchors.js.map +1 -0
  96. package/lib/features/routing/sticky-manhattan-edge-router.d.ts +107 -0
  97. package/lib/features/routing/sticky-manhattan-edge-router.d.ts.map +1 -0
  98. package/lib/features/routing/sticky-manhattan-edge-router.js +626 -0
  99. package/lib/features/routing/sticky-manhattan-edge-router.js.map +1 -0
  100. package/lib/features/search-palette/search-palette.d.ts +2 -2
  101. package/lib/features/search-palette/search-palette.d.ts.map +1 -1
  102. package/lib/features/search-palette/search-palette.js +3 -4
  103. package/lib/features/search-palette/search-palette.js.map +1 -1
  104. package/lib/features/select/select-module.d.ts +1 -1
  105. package/lib/features/select/select-module.js +1 -1
  106. package/lib/features/tool-palette/tool-palette.d.ts.map +1 -1
  107. package/lib/features/tool-palette/tool-palette.js +1 -2
  108. package/lib/features/tool-palette/tool-palette.js.map +1 -1
  109. package/lib/features/tools/change-bounds/change-bounds-tool-move-feedback.d.ts +2 -3
  110. package/lib/features/tools/change-bounds/change-bounds-tool-move-feedback.d.ts.map +1 -1
  111. package/lib/features/tools/change-bounds/change-bounds-tool-move-feedback.js +2 -3
  112. package/lib/features/tools/change-bounds/change-bounds-tool-move-feedback.js.map +1 -1
  113. package/lib/features/viewport/origin-viewport.d.ts +2 -13
  114. package/lib/features/viewport/origin-viewport.d.ts.map +1 -1
  115. package/lib/features/viewport/origin-viewport.js +3 -19
  116. package/lib/features/viewport/origin-viewport.js.map +1 -1
  117. package/lib/index.d.ts +11 -1
  118. package/lib/index.d.ts.map +1 -1
  119. package/lib/index.js +11 -1
  120. package/lib/index.js.map +1 -1
  121. package/lib/views/gedge-view-with-gaps-on-intersections.d.ts +39 -0
  122. package/lib/views/gedge-view-with-gaps-on-intersections.d.ts.map +1 -0
  123. package/lib/views/gedge-view-with-gaps-on-intersections.js +85 -0
  124. package/lib/views/gedge-view-with-gaps-on-intersections.js.map +1 -0
  125. package/lib/views/rounded-corner-manhattan-edge-view.d.ts +58 -0
  126. package/lib/views/rounded-corner-manhattan-edge-view.d.ts.map +1 -0
  127. package/lib/views/rounded-corner-manhattan-edge-view.js +170 -0
  128. package/lib/views/rounded-corner-manhattan-edge-view.js.map +1 -0
  129. package/package.json +4 -6
  130. package/src/base/action-dispatcher.spec.ts +103 -3
  131. package/src/base/action-dispatcher.ts +50 -25
  132. package/src/base/default.module.ts +4 -0
  133. package/src/base/editor-context-service.ts +17 -5
  134. package/src/base/model/diagram-loader.ts +8 -6
  135. package/src/base/model/glsp-model-source.ts +35 -1
  136. package/src/base/selection-service.ts +11 -2
  137. package/src/base/shortcuts/available-shortcuts-extension.ts +3 -4
  138. package/src/base/view/mouse-tool.ts +4 -19
  139. package/src/features/change-bounds/move-element-handler.ts +3 -2
  140. package/src/features/change-bounds/resize/resize-handler.ts +4 -3
  141. package/src/features/export/default-png-diagram-exporter.ts +145 -0
  142. package/src/features/export/default-svg-diagram-exporter.ts +39 -0
  143. package/src/features/export/diagram-export-postprocessor.ts +82 -0
  144. package/src/features/export/diagram-exporter.ts +45 -0
  145. package/src/features/export/export-modules.ts +24 -4
  146. package/src/features/export/export-result-action-handler.ts +66 -0
  147. package/src/features/export/export-svg-action-handler.ts +7 -2
  148. package/src/features/export/glsp-svg-exporter.ts +48 -12
  149. package/src/features/export/request-export-command.ts +65 -0
  150. package/src/features/export/request-export-key-listener.ts +29 -0
  151. package/src/features/helper-lines/helper-line-feedback.ts +6 -4
  152. package/src/features/routing/edge-router.ts +2 -2
  153. package/src/features/routing/routing-module.ts +8 -1
  154. package/src/features/routing/sticky-manhattan-anchors.ts +53 -0
  155. package/src/features/routing/sticky-manhattan-edge-router.spec.ts +212 -0
  156. package/src/features/routing/sticky-manhattan-edge-router.ts +713 -0
  157. package/src/features/search-palette/search-palette.ts +3 -4
  158. package/src/features/select/select-module.ts +1 -1
  159. package/src/features/tool-palette/tool-palette.ts +1 -1
  160. package/src/features/tools/change-bounds/change-bounds-tool-move-feedback.ts +13 -3
  161. package/src/features/viewport/origin-viewport.ts +10 -23
  162. package/src/index.ts +11 -1
  163. package/src/views/gedge-view-with-gaps-on-intersections.tsx +99 -0
  164. package/src/views/rounded-corner-manhattan-edge-view.ts +171 -0
@@ -21,7 +21,9 @@ import {
21
21
  Deferred,
22
22
  EMPTY_ROOT,
23
23
  GModelRoot,
24
+ HandleActionResult,
24
25
  IActionDispatcher,
26
+ MaybePromise,
25
27
  RejectAction,
26
28
  RequestAction,
27
29
  ResponseAction,
@@ -132,17 +134,15 @@ export class GLSPActionDispatcher extends ActionDispatcher implements IGModelRoo
132
134
  }
133
135
 
134
136
  protected async handleResponseAction(action: ResponseAction): Promise<void> {
135
- // clear any pending timeout
136
137
  const timeout = this.timeouts.get(action.responseId);
137
138
  if (timeout !== undefined) {
138
139
  clearTimeout(timeout);
139
140
  this.timeouts.delete(action.responseId);
140
141
  }
141
142
 
142
- // check for matching request
143
143
  const request = this.requests.get(action.responseId);
144
144
  if (!request) {
145
- // treat no matching request by dispatching action normally
145
+ // No pending request: re-dispatch as a normal action.
146
146
  this.logger.log(this, 'No matching request for response, dispatch normally', action);
147
147
  action.responseId = '';
148
148
  return this.handleAction(action);
@@ -150,7 +150,6 @@ export class GLSPActionDispatcher extends ActionDispatcher implements IGModelRoo
150
150
 
151
151
  this.requests.delete(action.responseId);
152
152
  if (RejectAction.is(action)) {
153
- // translation reject action to request rejection
154
153
  request.reject(new Error(action.message));
155
154
  this.logger.warn(this, `Request with id ${action.responseId} failed.`, action.message, action.detail);
156
155
  } else {
@@ -169,34 +168,47 @@ export class GLSPActionDispatcher extends ActionDispatcher implements IGModelRoo
169
168
  }
170
169
  this.logger.warn(this, 'Missing handler for action', action);
171
170
  const error = new Error(`Missing handler for action '${action.kind}'`);
172
- if (!RequestAction.is(action)) {
173
- throw error;
174
- }
175
- const request = this.requests.get(action.requestId);
176
- if (request !== undefined) {
177
- this.requests.delete(action.requestId);
178
- request.reject(error);
171
+ if (RequestAction.is(action)) {
172
+ const request = this.requests.get(action.requestId);
173
+ if (request !== undefined) {
174
+ this.requests.delete(action.requestId);
175
+ request.reject(error);
176
+ }
179
177
  }
178
+ throw error;
180
179
  }
181
180
 
182
181
  protected async handleActionWithHandler(action: Action, handlers: ActionHandler[]): Promise<void> {
183
182
  this.logger.log(this, 'Handle', action);
184
- const handlerResults: Promise<any>[] = [];
183
+ // No `await` inside the loop: invoke handlers in one burst and await their results collectively.
184
+ const handlerResults: PromiseLike<unknown>[] = [];
185
185
  for (const handler of handlers) {
186
- const result = await handler.handle(action);
187
- if (Action.is(result)) {
188
- handlerResults.push(this.dispatch(result));
189
- } else if (result !== undefined) {
190
- this.blockUntil = result.blockUntil;
191
- handlerResults.push(this.commandStack.execute(result));
186
+ const maybeResult = handler.handle(action);
187
+ if (MaybePromise.isPromise(maybeResult)) {
188
+ handlerResults.push(maybeResult.then(result => this.processHandlerResult(result)));
189
+ } else {
190
+ const resultPromise = this.processHandlerResult(maybeResult);
191
+ if (resultPromise) {
192
+ handlerResults.push(resultPromise);
193
+ }
192
194
  }
193
195
  }
194
196
  await Promise.all(handlerResults);
195
197
  }
196
198
 
199
+ protected processHandlerResult(result: HandleActionResult): Promise<unknown> | undefined {
200
+ if (Action.is(result)) {
201
+ return this.dispatch(result);
202
+ }
203
+ if (result !== undefined) {
204
+ this.blockUntil = result.blockUntil;
205
+ return this.commandStack.execute(result);
206
+ }
207
+ return undefined;
208
+ }
209
+
197
210
  override request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res> {
198
- if (!action.requestId && action.requestId === '') {
199
- // No request id has been specified. So we use a generated one.
211
+ if (!action.requestId || action.requestId === '') {
200
212
  action.requestId = RequestAction.generateRequestId();
201
213
  }
202
214
  return super.request(action);
@@ -214,19 +226,20 @@ export class GLSPActionDispatcher extends ActionDispatcher implements IGModelRoo
214
226
  */
215
227
  requestUntil<Res extends ResponseAction>(
216
228
  action: RequestAction<Res>,
217
- timeoutMs = 2000,
229
+ timeoutMs: number = action.timeout ?? 2000,
218
230
  rejectOnTimeout = false
219
231
  ): Promise<Res | undefined> {
220
- if (!action.requestId && action.requestId === '') {
221
- // No request id has been specified. So we use a generated one.
232
+ if (!action.requestId || action.requestId === '') {
222
233
  action.requestId = RequestAction.generateRequestId();
223
234
  }
235
+ // Stamp the effective timeout onto the action so the receiving side
236
+ // (handleServerRequest/handleClientRequest) can respect it.
237
+ action.timeout = timeoutMs;
224
238
 
225
239
  const requestId = action.requestId;
226
240
  const timeout = setTimeout(() => {
227
241
  const deferred = this.requests.get(requestId);
228
242
  if (deferred !== undefined) {
229
- // cleanup
230
243
  clearTimeout(timeout);
231
244
  this.requests.delete(requestId);
232
245
 
@@ -240,6 +253,18 @@ export class GLSPActionDispatcher extends ActionDispatcher implements IGModelRoo
240
253
  }
241
254
  }, timeoutMs);
242
255
  this.timeouts.set(requestId, timeout);
243
- return super.request<Res>(action);
256
+
257
+ const result = super.request<Res>(action);
258
+ // handleResponseAction only clears the timeout on the response path; clear it here on every
259
+ // other settle path (timeout, rejection, missing handler) so the timer and map entry can't leak.
260
+ const clearRequestTimeout = (): void => {
261
+ const pending = this.timeouts.get(requestId);
262
+ if (pending !== undefined) {
263
+ clearTimeout(pending);
264
+ this.timeouts.delete(requestId);
265
+ }
266
+ };
267
+ result.then(clearRequestTimeout, clearRequestTimeout);
268
+ return result;
244
269
  }
245
270
  }
@@ -16,6 +16,8 @@
16
16
  import {
17
17
  ActionHandlerRegistry,
18
18
  FeatureModule,
19
+ GetEditorContextAction,
20
+ GetSelectionAction,
19
21
  KeyTool,
20
22
  LocationPostprocessor,
21
23
  MousePositionTracker,
@@ -83,6 +85,7 @@ export const defaultModule = new FeatureModule(
83
85
 
84
86
  configureActionHandler(context, SetEditModeAction.KIND, EditorContextService);
85
87
  configureActionHandler(context, SetDirtyStateAction.KIND, EditorContextService);
88
+ configureActionHandler(context, GetEditorContextAction.KIND, EditorContextService);
86
89
 
87
90
  bind(FocusTracker).toSelf().inSingletonScope();
88
91
  bind(TYPES.IDiagramStartup).toService(FocusTracker);
@@ -121,6 +124,7 @@ export const defaultModule = new FeatureModule(
121
124
  bind(SelectionService).toSelf().inSingletonScope();
122
125
  bind(TYPES.IGModelRootListener).toService(SelectionService);
123
126
  bind(TYPES.IDiagramStartup).toService(SelectionService);
127
+ configureActionHandler(context, GetSelectionAction.KIND, SelectionService);
124
128
 
125
129
  // Feedback Support ------------------------------------
126
130
  // Generic re-usable feedback modifying css classes
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2020-2025 EclipseSource and others.
2
+ * Copyright (c) 2020-2026 EclipseSource and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -21,14 +21,14 @@ import {
21
21
  DisposableCollection,
22
22
  EditMode,
23
23
  EditorContext,
24
+ EditorContextResult,
24
25
  Emitter,
25
26
  Event,
26
- findParentByFeature,
27
27
  GModelElement,
28
28
  GModelRoot,
29
+ GetEditorContextAction,
29
30
  IActionDispatcher,
30
31
  IActionHandler,
31
- isViewport,
32
32
  LazyInjector,
33
33
  MaybePromise,
34
34
  MousePositionTracker,
@@ -37,7 +37,9 @@ import {
37
37
  SetEditModeAction,
38
38
  TYPES,
39
39
  ValueChange,
40
- Viewport
40
+ Viewport,
41
+ findParentByFeature,
42
+ isViewport
41
43
  } from '@eclipse-glsp/sprotty';
42
44
  import { inject, injectable, postConstruct, preDestroy } from 'inversify';
43
45
  import { FocusChange, FocusTracker } from './focus/focus-tracker';
@@ -177,6 +179,8 @@ export class EditorContextService implements IActionHandler, Disposable, IDiagra
177
179
  return {
178
180
  selectedElementIds: Array.from(this.selectionService.getSelectedElementIDs()),
179
181
  lastMousePosition: this.mousePositionTracker.lastPositionOnDiagram,
182
+ viewport: this.viewportData,
183
+ canvasBounds: this.canvasBounds,
180
184
  args
181
185
  };
182
186
  }
@@ -185,18 +189,26 @@ export class EditorContextService implements IActionHandler, Disposable, IDiagra
185
189
  return {
186
190
  selectedElementIds,
187
191
  lastMousePosition: this.mousePositionTracker.lastPositionOnDiagram,
192
+ viewport: this.viewportData,
193
+ canvasBounds: this.canvasBounds,
188
194
  args
189
195
  };
190
196
  }
191
197
 
192
- handle(action: Action): void {
198
+ handle(action: Action): Action | void {
193
199
  if (SetEditModeAction.is(action)) {
194
200
  this.handleSetEditModeAction(action);
195
201
  } else if (SetDirtyStateAction.is(action)) {
196
202
  this.handleSetDirtyStateAction(action);
203
+ } else if (GetEditorContextAction.is(action)) {
204
+ return this.handleGetEditorContext(action);
197
205
  }
198
206
  }
199
207
 
208
+ protected handleGetEditorContext(action: GetEditorContextAction): EditorContextResult {
209
+ return EditorContextResult.create(this.get(), { responseId: action.requestId });
210
+ }
211
+
200
212
  protected handleSetEditModeAction(action: SetEditModeAction): void {
201
213
  const oldValue = this._editMode;
202
214
  this._editMode = action.editMode;
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2023-2024 EclipseSource and others.
2
+ * Copyright (c) 2023-2026 EclipseSource and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -117,7 +117,7 @@ export namespace IDiagramStartup {
117
117
  }
118
118
  }
119
119
 
120
- export interface DiagramLoadingOptions {
120
+ export interface DiagramLoadingOptions<P extends InitializeParameters = InitializeParameters> {
121
121
  /**
122
122
  * Optional custom options that should be used the initial {@link RequestModelAction}.
123
123
  * These options will be merged with the default options (`diagramType` and `sourceUri`).
@@ -126,10 +126,12 @@ export interface DiagramLoadingOptions {
126
126
  requestModelOptions?: Args;
127
127
 
128
128
  /**
129
- * Optional partial {@link InitializeParameters} that should be used for {@link GLSPClient.initializeServer} request if the underlying
130
- * {@link GLSPClient} has not been initialized yet.
129
+ * Optional partial {@link InitializeParameters} that should be used for {@link GLSPClient.initializeServer}
130
+ * request if the underlying {@link GLSPClient} has not been initialized yet. The generic type parameter
131
+ * `P` lets callers narrow this to a more specific extension type (e.g., `McpInitializeParameters`)
132
+ * without leaking that extension into the base loader API.
131
133
  */
132
- initializeParameters?: Partial<InitializeParameters>;
134
+ initializeParameters?: Partial<P>;
133
135
 
134
136
  /**
135
137
  * Flag to enable/disable client side notifications during the loading process.
@@ -171,7 +173,7 @@ export class DiagramLoader {
171
173
  return this.lazyInjector.getAll<IDiagramStartup>(TYPES.IDiagramStartup);
172
174
  }
173
175
 
174
- async load(options: DiagramLoadingOptions = {}): Promise<void> {
176
+ async load<P extends InitializeParameters = InitializeParameters>(options: DiagramLoadingOptions<P> = {}): Promise<void> {
175
177
  this.diagramStartups.sort(Ranked.sort);
176
178
  await this.invokeStartupHook('preLoadDiagram');
177
179
  const resolvedOptions: ResolvedDiagramLoadingOptions = {
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2023-2024 EclipseSource and others.
2
+ * Copyright (c) 2023-2026 EclipseSource and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -22,10 +22,14 @@ import {
22
22
  DisposeClientSessionParameters,
23
23
  GLSPClient,
24
24
  GModelRootSchema,
25
+ IActionDispatcher,
25
26
  ILogger,
26
27
  InitializeClientSessionParameters,
27
28
  InitializeResult,
28
29
  ModelSource,
30
+ RejectAction,
31
+ RequestAction,
32
+ ResponseAction,
29
33
  TYPES
30
34
  } from '@eclipse-glsp/sprotty';
31
35
  import { inject, injectable, preDestroy } from 'inversify';
@@ -94,6 +98,8 @@ export class GLSPModelSource extends ModelSource implements Disposable {
94
98
  @inject(TYPES.IDiagramOptions)
95
99
  protected options: IDiagramOptions;
96
100
 
101
+ declare readonly actionDispatcher: IActionDispatcher;
102
+
97
103
  protected toDispose = new DisposableCollection();
98
104
  clientId: string;
99
105
 
@@ -164,9 +170,37 @@ export class GLSPModelSource extends ModelSource implements Disposable {
164
170
  const action = message.action;
165
171
  ServerAction.mark(action);
166
172
  this.logger.log(this, 'receiving', action);
173
+ if (RequestAction.is(action)) {
174
+ this.handleServerRequest(action);
175
+ return;
176
+ }
167
177
  this.actionDispatcher.dispatch(action);
168
178
  }
169
179
 
180
+ // Fire-and-forget: intentionally not awaited by messageReceived()
181
+ protected async handleServerRequest(action: RequestAction<ResponseAction>): Promise<void> {
182
+ try {
183
+ const response =
184
+ action.timeout !== undefined
185
+ ? await this.actionDispatcher.requestUntil(action, action.timeout, true)
186
+ : await this.actionDispatcher.request(action);
187
+ if (response) {
188
+ this.sendResponseToServer(response);
189
+ }
190
+ } catch (error) {
191
+ this.logger.error(this, `Failed to handle server request '${action.kind}' (${action.requestId})`, error);
192
+ const reject = RejectAction.create(`Failed to handle request '${action.kind}' (${action.requestId})`, {
193
+ responseId: action.requestId,
194
+ detail: error?.toString?.()
195
+ });
196
+ this.sendResponseToServer(reject);
197
+ }
198
+ }
199
+
200
+ protected sendResponseToServer(response: ResponseAction): void {
201
+ this.forwardToServer(response);
202
+ }
203
+
170
204
  override initialize(registry: GLSPActionHandlerRegistry): void {
171
205
  // Registering actions here is discouraged and it's recommended
172
206
  // to implemented dedicated action handlers.
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2019-2024 EclipseSource and others.
2
+ * Copyright (c) 2019-2026 EclipseSource and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -23,12 +23,15 @@ import {
23
23
  Emitter,
24
24
  Event,
25
25
  GChildElement,
26
+ GetSelectionAction,
26
27
  GModelElement,
27
28
  GModelRoot,
29
+ IActionHandler,
28
30
  ILogger,
29
31
  LazyInjector,
30
32
  SelectAction,
31
33
  SelectAllAction,
34
+ SelectionResult,
32
35
  SprottySelectAllCommand,
33
36
  SprottySelectCommand,
34
37
  TYPES,
@@ -60,7 +63,7 @@ export interface SelectionChange {
60
63
  }
61
64
 
62
65
  @injectable()
63
- export class SelectionService implements IGModelRootListener, Disposable, IDiagramStartup {
66
+ export class SelectionService implements IGModelRootListener, IActionHandler, Disposable, IDiagramStartup {
64
67
  @inject(TYPES.IFeedbackActionDispatcher)
65
68
  protected feedbackDispatcher: IFeedbackActionDispatcher;
66
69
 
@@ -84,6 +87,12 @@ export class SelectionService implements IGModelRootListener, Disposable, IDiagr
84
87
  this.lazyInjector.getAll<ISelectionListener>(TYPES.ISelectionListener).forEach(listener => this.addListener(listener));
85
88
  }
86
89
 
90
+ handle(action: Action): Action | void {
91
+ if (GetSelectionAction.is(action)) {
92
+ return SelectionResult.create(this.getSelectedElementIDs(), action.requestId);
93
+ }
94
+ }
95
+
87
96
  @preDestroy()
88
97
  dispose(): void {
89
98
  this.toDispose.dispose();
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2023-2025 Business Informatics Group (TU Wien) and others.
2
+ * Copyright (c) 2023-2026 Business Informatics Group (TU Wien) and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -14,9 +14,8 @@
14
14
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
15
  ********************************************************************************/
16
16
 
17
- import { GModelRoot, matchesKeystroke, TYPES } from '@eclipse-glsp/sprotty';
17
+ import { GModelRoot, groupBy, matchesKeystroke, TYPES } from '@eclipse-glsp/sprotty';
18
18
  import { inject, injectable } from 'inversify';
19
- import { groupBy } from 'lodash';
20
19
  import { GLSPAbstractUIExtension } from '../../base/ui-extension/ui-extension';
21
20
  import { messages } from '../messages';
22
21
  import type { IShortcutManager, ShortcutRegistration } from './shortcuts-manager';
@@ -79,7 +78,7 @@ export class AvailableShortcutsUIExtension extends GLSPAbstractUIExtension {
79
78
  headerRow.appendChild(keybindingCell);
80
79
  tableHead.appendChild(headerRow);
81
80
 
82
- for (const [group, shortcuts] of Object.entries(grouped)) {
81
+ for (const [group, shortcuts] of grouped.entries()) {
83
82
  tableBody.appendChild(this.createGroupHeader(group));
84
83
  shortcuts.forEach(s => {
85
84
  tableBody.appendChild(this.createEntry(s));
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2019-2024 EclipseSource and others.
2
+ * Copyright (c) 2019-2026 EclipseSource and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -18,6 +18,7 @@ import {
18
18
  Disposable,
19
19
  GModelElement,
20
20
  GModelRoot,
21
+ groupBy,
21
22
  LazyInjector,
22
23
  MaybePromise,
23
24
  MouseListener,
@@ -51,7 +52,7 @@ export class GLSPMouseTool extends MouseTool implements IDiagramStartup {
51
52
 
52
53
  override register(mouseListener: MouseListener): void {
53
54
  super.register(mouseListener);
54
- this.rankedMouseListeners = groupBy(this.mouseListeners, listener => Ranked.getRank(listener));
55
+ this.rankedMouseListeners = groupBy(this.mouseListeners, listener => Ranked.getRank(listener), true);
55
56
  }
56
57
 
57
58
  registerListener(mouseListener: MouseListener): Disposable {
@@ -61,7 +62,7 @@ export class GLSPMouseTool extends MouseTool implements IDiagramStartup {
61
62
 
62
63
  override deregister(mouseListener: MouseListener): void {
63
64
  super.deregister(mouseListener);
64
- this.rankedMouseListeners = groupBy(this.mouseListeners, listener => Ranked.getRank(listener));
65
+ this.rankedMouseListeners = groupBy(this.mouseListeners, listener => Ranked.getRank(listener), true);
65
66
  }
66
67
 
67
68
  protected override handleEvent<K extends MouseListenerMethods>(methodName: K, model: GModelRoot, event: MouseEvent): void {
@@ -104,19 +105,3 @@ export class GLSPMouseTool extends MouseTool implements IDiagramStartup {
104
105
  }
105
106
  }
106
107
  }
107
-
108
- function groupBy<K, T>(array: Array<T>, keyFunction: (x: T) => K): Map<K, T[]> {
109
- const unsortedMap = array.reduce((result: Map<K, T[]>, item: T) => {
110
- const key = keyFunction(item);
111
- if (!result.has(key)) {
112
- result.set(key, [item]);
113
- } else {
114
- const entries = result.get(key);
115
- if (entries) {
116
- entries.push(item);
117
- }
118
- }
119
- return result;
120
- }, new Map<K, T[]>());
121
- return new Map<K, T[]>([...unsortedMap.entries()].sort());
122
- }
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2023-2025 Business Informatics Group (TU Wien) and others.
2
+ * Copyright (c) 2023-2026 Business Informatics Group (TU Wien) and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -17,6 +17,7 @@
17
17
  import {
18
18
  Action,
19
19
  ChangeBoundsOperation,
20
+ DebouncedFunc,
20
21
  ElementAndBounds,
21
22
  ElementMove,
22
23
  IActionDispatcher,
@@ -27,10 +28,10 @@ import {
27
28
  MoveViewportAction,
28
29
  Point,
29
30
  TYPES,
31
+ debounce,
30
32
  isBoundsAware
31
33
  } from '@eclipse-glsp/sprotty';
32
34
  import { inject, injectable, optional, postConstruct } from 'inversify';
33
- import { DebouncedFunc, debounce } from 'lodash';
34
35
  import { EditorContextService } from '../../base/editor-context-service';
35
36
  import { IFeedbackActionDispatcher } from '../../base/feedback/feedback-action-dispatcher';
36
37
  import { FeedbackEmitter } from '../../base/feedback/feedback-emitter';
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2023-2025 Business Informatics Group (TU Wien) and others.
2
+ * Copyright (c) 2023-2026 Business Informatics Group (TU Wien) and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -17,6 +17,7 @@
17
17
  import {
18
18
  Action,
19
19
  ChangeBoundsOperation,
20
+ DebouncedFunc,
20
21
  Dimension,
21
22
  ElementAndBounds,
22
23
  GModelElement,
@@ -27,10 +28,10 @@ import {
27
28
  ISnapper,
28
29
  Point,
29
30
  SetBoundsAction,
30
- TYPES
31
+ TYPES,
32
+ debounce
31
33
  } from '@eclipse-glsp/sprotty';
32
34
  import { inject, injectable, optional, postConstruct } from 'inversify';
33
- import { DebouncedFunc, debounce } from 'lodash';
34
35
  import { EditorContextService } from '../../../base/editor-context-service';
35
36
  import { IFeedbackActionDispatcher } from '../../../base/feedback/feedback-action-dispatcher';
36
37
  import { FeedbackEmitter } from '../../../base/feedback/feedback-emitter';
@@ -0,0 +1,145 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { Action, ExportFormat, ExportMimeType, GModelRoot, PngExportOptions } from '@eclipse-glsp/sprotty';
18
+ import { inject, injectable } from 'inversify';
19
+ import { DiagramExporter } from './diagram-exporter';
20
+ import { GLSPSvgExporter } from './glsp-svg-exporter';
21
+
22
+ /** Default raster width (in CSS px) when neither `width` nor `height` is specified. */
23
+ const DEFAULT_PNG_WIDTH = 1024;
24
+ const DEFAULT_PNG_BACKGROUND = 'white';
25
+
26
+ /**
27
+ * Default PNG strategy for the unified export registry. Reuses {@link GLSPSvgExporter}'s
28
+ * SVG-rendering pipeline and rasterises the result client-side via `OffscreenCanvas`.
29
+ * Adopters that need server-side rendering replace this binding with their own strategy.
30
+ */
31
+ @injectable()
32
+ export class DefaultPngDiagramExporter implements DiagramExporter<PngExportOptions> {
33
+ readonly format: ExportFormat = 'png';
34
+ readonly mimeType: ExportMimeType = 'image/png';
35
+ readonly encoding = 'base64' as const;
36
+
37
+ @inject(GLSPSvgExporter) protected svgExporter: GLSPSvgExporter;
38
+
39
+ async export(root: GModelRoot, options: PngExportOptions = {}, cause?: Action): Promise<string> {
40
+ if (typeof document === 'undefined' || typeof OffscreenCanvas === 'undefined') {
41
+ throw new Error('PNG export failed: requires a DOM environment with OffscreenCanvas support');
42
+ }
43
+ const svgString = this.svgExporter.exportToString(root, options, cause);
44
+ const { width, height } = this.computeDimensions(svgString, options);
45
+ // Pin the SVG root to the target raster size BEFORE loading into `<img>` so the browser
46
+ // rasterises the vector content at full resolution.
47
+ const sizedSvg = this.applyRasterSize(svgString, width, height);
48
+ const blob = await this.rasterise(sizedSvg, width, height, options.background ?? DEFAULT_PNG_BACKGROUND);
49
+ return this.blobToBase64(blob);
50
+ }
51
+
52
+ /**
53
+ * Rewrite the root `<svg>` tag so its rendered size matches the target raster dimensions.
54
+ * Replaces `width`/`height` attributes and strips conflicting `style` declarations
55
+ * (the SVG exporter emits `width: …px !important` on the inline style for viewer-friendly
56
+ * display, which would otherwise win over the attributes). The existing `viewBox`
57
+ * preserves the vector coordinate system so the content scales without distortion.
58
+ */
59
+ protected applyRasterSize(svgString: string, width: number, height: number): string {
60
+ return svgString.replace(/<svg\b([^>]*)>/, (_, attrs: string) => {
61
+ const cleaned = attrs
62
+ .replace(/\swidth="[^"]*"/, '')
63
+ .replace(/\sheight="[^"]*"/, '')
64
+ .replace(/style="([^"]*)"/, (_match, styleBody: string) => {
65
+ const stripped = styleBody
66
+ .replace(/(?:^|;)\s*width\s*:[^;]*/gi, '')
67
+ .replace(/(?:^|;)\s*height\s*:[^;]*/gi, '')
68
+ .replace(/^\s*;+/, '')
69
+ .trim();
70
+ return stripped ? `style="${stripped}"` : '';
71
+ });
72
+ return `<svg${cleaned} width="${width}" height="${height}">`;
73
+ });
74
+ }
75
+
76
+ protected computeDimensions(svgString: string, options: PngExportOptions): { width: number; height: number } {
77
+ const requestedWidth = options.width ?? DEFAULT_PNG_WIDTH;
78
+ const requestedHeight = options.height;
79
+ if (requestedHeight !== undefined) {
80
+ return { width: requestedWidth, height: requestedHeight };
81
+ }
82
+ // Read the aspect from the SVG attributes directly to avoid a redundant bitmap decode.
83
+ const aspect = this.parseAspectRatio(svgString);
84
+ return { width: requestedWidth, height: requestedWidth / aspect };
85
+ }
86
+
87
+ /**
88
+ * Read the aspect ratio from the root `<svg>` tag's `viewBox`, falling back to the
89
+ * `width`/`height` attributes. GLSP-emitted SVGs always carry a `viewBox` (set by the
90
+ * underlying sprotty exporter), so the fallback is a defensive safeguard.
91
+ */
92
+ protected parseAspectRatio(svgString: string): number {
93
+ const root = svgString.match(/<svg\b([^>]*)>/);
94
+ const attrs = root?.[1] ?? '';
95
+ const viewBox = attrs.match(/viewBox\s*=\s*"\s*[\d.eE+-]+\s+[\d.eE+-]+\s+([\d.eE+-]+)\s+([\d.eE+-]+)\s*"/);
96
+ if (viewBox) {
97
+ return parseFloat(viewBox[1]) / parseFloat(viewBox[2]);
98
+ }
99
+ const widthAttr = attrs.match(/\swidth\s*=\s*"([\d.eE+-]+)/);
100
+ const heightAttr = attrs.match(/\sheight\s*=\s*"([\d.eE+-]+)/);
101
+ if (widthAttr && heightAttr) {
102
+ return parseFloat(widthAttr[1]) / parseFloat(heightAttr[1]);
103
+ }
104
+ throw new Error('PNG export failed: SVG missing viewBox/width/height — cannot determine aspect ratio');
105
+ }
106
+
107
+ protected async rasterise(svgString: string, width: number, height: number, background: string): Promise<Blob> {
108
+ const url = URL.createObjectURL(new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }));
109
+ try {
110
+ const bitmap = await this.bitmapFromSvgUrl(url);
111
+ const offscreen = new OffscreenCanvas(width, height);
112
+ const ctx = offscreen.getContext('2d');
113
+ if (!ctx) {
114
+ throw new Error('PNG export failed: 2D context unavailable on OffscreenCanvas');
115
+ }
116
+ ctx.fillStyle = background;
117
+ ctx.fillRect(0, 0, width, height);
118
+ ctx.drawImage(bitmap, 0, 0, width, height);
119
+ return offscreen.convertToBlob({ type: 'image/png' });
120
+ } finally {
121
+ URL.revokeObjectURL(url);
122
+ }
123
+ }
124
+
125
+ protected bitmapFromSvgUrl(url: string): Promise<ImageBitmap> {
126
+ return new Promise((resolve, reject) => {
127
+ const img = new Image();
128
+ img.onload = () => createImageBitmap(img).then(resolve, reject);
129
+ img.onerror = () => reject(new Error('PNG export failed: SVG could not be loaded into Image'));
130
+ img.src = url;
131
+ });
132
+ }
133
+
134
+ protected blobToBase64(blob: Blob): Promise<string> {
135
+ return new Promise((resolve, reject) => {
136
+ const reader = new FileReader();
137
+ reader.onloadend = () => {
138
+ const result = reader.result as string;
139
+ resolve(result.replace(/^data:[^;]+;base64,/, ''));
140
+ };
141
+ reader.onerror = () => reject(reader.error ?? new Error('PNG export failed: FileReader error'));
142
+ reader.readAsDataURL(blob);
143
+ });
144
+ }
145
+ }