@eclipse-glsp/client 2.1.0-next.303 → 2.1.0-next.305

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eclipse-glsp/client",
3
- "version": "2.1.0-next.303+f286b9d",
3
+ "version": "2.1.0-next.305+87e78dc",
4
4
  "description": "A sprotty-based client for GLSP",
5
5
  "keywords": [
6
6
  "eclipse",
@@ -47,7 +47,7 @@
47
47
  "watch": "tsc -w"
48
48
  },
49
49
  "dependencies": {
50
- "@eclipse-glsp/sprotty": "2.1.0-next.303+f286b9d",
50
+ "@eclipse-glsp/sprotty": "2.1.0-next.305+87e78dc",
51
51
  "autocompleter": "^9.1.0",
52
52
  "file-saver": "^2.0.5",
53
53
  "lodash": "4.17.21"
@@ -59,5 +59,5 @@
59
59
  "publishConfig": {
60
60
  "access": "public"
61
61
  },
62
- "gitHead": "f286b9d8289e11b2b3760cc7947c3bfc12a4bd70"
62
+ "gitHead": "87e78dc75c8346396619bbb1126335860f9fb50b"
63
63
  }
@@ -13,8 +13,9 @@
13
13
  *
14
14
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
15
  ********************************************************************************/
16
- import { inject, injectable } from 'inversify';
17
16
  import { Action, ActionDispatcher, RequestAction, ResponseAction } from '@eclipse-glsp/sprotty';
17
+ import { inject, injectable } from 'inversify';
18
+ import { OptionalAction } from './model/glsp-model-source';
18
19
  import { ModelInitializationConstraint } from './model/model-initialization-constraint';
19
20
 
20
21
  @injectable()
@@ -76,6 +77,9 @@ export class GLSPActionDispatcher extends ActionDispatcher {
76
77
  action.responseId = '';
77
78
  }
78
79
  }
80
+ if (!this.hasHandler(action) && OptionalAction.is(action)) {
81
+ return Promise.resolve();
82
+ }
79
83
  return super.handleAction(action);
80
84
  }
81
85
 
@@ -14,7 +14,6 @@
14
14
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
15
  ********************************************************************************/
16
16
 
17
- import { inject, injectable, preDestroy } from 'inversify';
18
17
  import {
19
18
  Action,
20
19
  ActionMessage,
@@ -29,8 +28,10 @@ import {
29
28
  ModelSource,
30
29
  TYPES
31
30
  } from '@eclipse-glsp/sprotty';
31
+ import { inject, injectable, preDestroy } from 'inversify';
32
32
  import { GLSPActionHandlerRegistry } from '../action-handler-registry';
33
33
  import { IDiagramOptions } from './diagram-loader';
34
+
34
35
  /**
35
36
  * A helper interface that allows the client to mark actions that have been received from the server.
36
37
  */
@@ -44,7 +45,7 @@ export namespace ServerAction {
44
45
  }
45
46
 
46
47
  /**
47
- * Mark the given action as {@link ServerAction} by attaching the "_receivedFromServer" property
48
+ * Mark the given action as {@link ServerAction} by attaching the "__receivedFromServer" property
48
49
  * @param action The action that should be marked as server action
49
50
  */
50
51
  export function mark(action: Action): void {
@@ -52,6 +53,29 @@ export namespace ServerAction {
52
53
  }
53
54
  }
54
55
 
56
+ /**
57
+ * A helper interface that allows the client to mark actions that can be considered optional and should not throw an error if
58
+ * no handler is available.
59
+ */
60
+ export interface OptionalAction extends Action {
61
+ __skipErrorIfNoHandler: true;
62
+ }
63
+
64
+ export namespace OptionalAction {
65
+ export function is(object: unknown): object is ServerAction {
66
+ return Action.is(object) && '__skipErrorIfNoHandler' in object && object.__skipErrorIfNoHandler === true;
67
+ }
68
+
69
+ /**
70
+ * Mark the given action as {@link OptionalAction} by attaching the "__skipErrorIfNoHandler" property
71
+ * @param action The action that should be marked as optional action
72
+ */
73
+ export function mark<T extends Action>(action: T): T & OptionalAction {
74
+ (action as unknown as OptionalAction).__skipErrorIfNoHandler = true;
75
+ return action as T & OptionalAction;
76
+ }
77
+ }
78
+
55
79
  /**
56
80
  * Central component for enabling the client-server action flow with the help of an underlying {@link GLSPClient}.
57
81
  * Handles & forwards actions that are intended for the GLSP server. In addition, it handles {@link ActionMessage}s received
@@ -13,10 +13,10 @@
13
13
  *
14
14
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
15
  ********************************************************************************/
16
+ import { Action, Disposable, GModelRoot, GNode, TYPES, initializeContainer } from '@eclipse-glsp/sprotty';
16
17
  import { AssertionError, expect } from 'chai';
17
18
  import { Container, injectable } from 'inversify';
18
19
  import * as sinon from 'sinon';
19
- import { Action, Disposable, GModelRoot, GNode, TYPES, initializeContainer } from '@eclipse-glsp/sprotty';
20
20
  import { defaultModule } from './default.module';
21
21
  import { IFeedbackActionDispatcher, IFeedbackEmitter } from './feedback/feedback-action-dispatcher';
22
22
  import { ISelectionListener, SelectFeedbackAction, SelectionService } from './selection-service';
@@ -217,13 +217,13 @@ describe('SelectionService', () => {
217
217
  selectionService.updateSelection(root, [], ['node4']);
218
218
  assertListener(listener, root, ['node2', 'node3'], ['node4'], 4);
219
219
  });
220
- it('A registered listener should be notified of root changes.', () => {
220
+ it('A registered listener should NOT be notified of root changes.', () => {
221
221
  selectionService.updateSelection(root, [], []);
222
- assertListener(listener, root, [], [], 1);
222
+ assertListener(listener, root, [], [], 0);
223
223
 
224
224
  const newRoot = createRoot('node1', 'newNode2', 'newNode3');
225
225
  selectionService.updateSelection(newRoot, [], []);
226
- assertListener(listener, newRoot, [], [], 2);
226
+ assertListener(listener, newRoot, [], [], 0);
227
227
  });
228
228
  it('Selecting the same elements consecutively should not trigger a listener update.', () => {
229
229
  selectionService.updateSelection(root, ['node1'], []);
@@ -290,8 +290,10 @@ describe('SelectionService', () => {
290
290
  expectedCalled: number
291
291
  ): void {
292
292
  expect(listener.selectionChanged.callCount).to.be.equal(expectedCalled);
293
- expect(listener.selectionChanged.lastCall.args[0]).to.be.deep.equals(expectedRoot);
294
- expect(listener.selectionChanged.lastCall.args[1]).to.be.deep.equals(expectedSelection);
295
- expect(listener.selectionChanged.lastCall.args[2]).to.be.deep.equals(expectedDeselection);
293
+ if (expectedCalled > 0) {
294
+ expect(listener.selectionChanged.lastCall.args[0]).to.be.deep.equals(expectedRoot);
295
+ expect(listener.selectionChanged.lastCall.args[1]).to.be.deep.equals(expectedSelection);
296
+ expect(listener.selectionChanged.lastCall.args[2]).to.be.deep.equals(expectedDeselection);
297
+ }
296
298
  }
297
299
  });
@@ -96,41 +96,41 @@ export class SelectionService implements IGModelRootListener, Disposable {
96
96
  this.updateSelection(root, [], []);
97
97
  }
98
98
 
99
- updateSelection(root: Readonly<GModelRoot>, select: string[], deselect: string[]): void {
100
- if (root === undefined && select.length === 0 && deselect.length === 0) {
99
+ updateSelection(newRoot: Readonly<GModelRoot>, select: string[], deselect: string[]): void {
100
+ if (newRoot === undefined && select.length === 0 && deselect.length === 0) {
101
101
  return;
102
102
  }
103
103
  const prevRoot = this.root;
104
104
  const prevSelectedElementIDs = new Set(this.selectedElementIDs);
105
105
 
106
- // update root
107
- this.root = root;
106
+ this.root = newRoot;
108
107
 
109
- // update selected element IDs and collect deselected elements
110
- // - select all elements that are not deselected at the same time (no-op)
111
- // - deselect all elements that are not selected at the same time (no-op) but was selected
108
+ // We only select elements that are not part of the deselection
112
109
  const toSelect = [...select].filter(selectId => deselect.indexOf(selectId) === -1);
110
+
111
+ // We only need to deselect elements that are not part of the selection
112
+ // If an element is part of both the select and deselect, it's state is not changed
113
113
  const toDeselect = [...deselect].filter(deselectId => select.indexOf(deselectId) === -1 && this.selectedElementIDs.has(deselectId));
114
- for (const id of toDeselect) {
115
- this.selectedElementIDs.delete(id);
116
- }
117
- for (const id of toSelect) {
118
- this.selectedElementIDs.add(id);
119
- }
120
114
 
115
+ // update selected element ids
116
+ toDeselect.forEach(toDeselectId => this.selectedElementIDs.delete(toDeselectId));
117
+ toSelect.forEach(toSelectId => this.selectedElementIDs.add(toSelectId));
118
+
119
+ // check if the newly or previously selected elements still exist in the updated root
121
120
  const deselectedElementIDs = new Set(toDeselect);
122
- // see if selected elements still exist in the updated root
123
121
  for (const id of this.selectedElementIDs) {
124
- const element = root.index.getById(id);
122
+ const element = newRoot.index.getById(id);
125
123
  if (element === undefined) {
124
+ // element to be selected does not exist in the root...
126
125
  this.selectedElementIDs.delete(id);
127
- if (prevRoot !== undefined && prevRoot.index.getById(id)) {
126
+ if (prevRoot?.index.getById(id)) {
127
+ // ...but existed in the previous root, so we want to consider it deselected
128
128
  deselectedElementIDs.add(id);
129
129
  }
130
130
  }
131
131
  }
132
132
 
133
- // only send out changes if there actually are changes, i.e., the root or the selected elements changed
133
+ // only send out changes if there actually are changes, i.e., any of the selected elements ids has changed
134
134
  const selectionChanged =
135
135
  prevSelectedElementIDs.size !== this.selectedElementIDs.size ||
136
136
  ![...prevSelectedElementIDs].every(value => this.selectedElementIDs.has(value));
@@ -142,10 +142,6 @@ export class SelectionService implements IGModelRootListener, Disposable {
142
142
  deselectedElementsIDs: [...deselectedElementIDs]
143
143
  })
144
144
  ]);
145
- }
146
-
147
- const rootChanged = prevRoot !== root;
148
- if (rootChanged || selectionChanged) {
149
145
  // notify listeners after the feedback action
150
146
  this.notifyListeners(this.root, this.selectedElementIDs, deselectedElementIDs);
151
147
  }
@@ -36,6 +36,7 @@ import { DebouncedFunc, debounce } from 'lodash';
36
36
  import { DragAwareMouseListener } from '../../../base/drag-aware-mouse-listener';
37
37
  import { CursorCSS, cursorFeedbackAction } from '../../../base/feedback/css-feedback';
38
38
  import { FeedbackCommand } from '../../../base/feedback/feedback-command';
39
+ import { OptionalAction } from '../../../base/model/glsp-model-source';
39
40
  import { forEachElement } from '../../../utils/gmodel-util';
40
41
  import { SResizeHandle, addResizeHandles, isResizable, removeResizeHandles } from '../../change-bounds/model';
41
42
  import { createMovementRestrictionFeedback, removeMovementRestrictionFeedback } from '../../change-bounds/movement-restrictor';
@@ -125,7 +126,7 @@ export namespace MoveInitializedEventAction {
125
126
  }
126
127
 
127
128
  export function create(): MoveInitializedEventAction {
128
- return { kind: KIND };
129
+ return OptionalAction.mark({ kind: KIND });
129
130
  }
130
131
  }
131
132
 
@@ -141,7 +142,7 @@ export namespace MoveFinishedEventAction {
141
142
  }
142
143
 
143
144
  export function create(): MoveFinishedEventAction {
144
- return { kind: KIND };
145
+ return OptionalAction.mark({ kind: KIND });
145
146
  }
146
147
  }
147
148
 
@@ -157,6 +158,7 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
157
158
  protected positionUpdater;
158
159
  protected elementId2startPos = new Map<string, Point>();
159
160
  protected pendingMoveInitialized?: DebouncedFunc<() => void>;
161
+ protected moveInitialized = false;
160
162
 
161
163
  constructor(protected tool: ChangeBoundsTool) {
162
164
  super();
@@ -178,10 +180,12 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
178
180
  }
179
181
 
180
182
  protected scheduleMoveInitialized(): void {
183
+ this.moveInitialized = false;
181
184
  this.pendingMoveInitialized?.cancel();
182
185
  this.pendingMoveInitialized = debounce(() => {
183
186
  this.tool.registerFeedback([MoveInitializedEventAction.create()], this);
184
187
  this.pendingMoveInitialized = undefined;
188
+ this.moveInitialized = true;
185
189
  }, 750);
186
190
  this.pendingMoveInitialized();
187
191
  }
@@ -329,11 +333,12 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
329
333
  const moveAction = MoveAction.create(elementMoves, { animate: false, finished: true });
330
334
  this.tool.deregisterFeedback(this, [moveAction]);
331
335
  }
332
- } else if (resetFeedback) {
336
+ } else if (resetFeedback && this.moveInitialized) {
333
337
  this.tool.deregisterFeedback(this, [MoveFinishedEventAction.create()]);
334
338
  }
335
339
  this.positionUpdater.resetPosition();
336
340
  this._isMouseDrag = false;
341
+ this.moveInitialized = false;
337
342
  this.rootElement = undefined;
338
343
  this.elementId2startPos.clear();
339
344
  }