@hydranium/glsp-server 1.0.0-next.31 → 1.0.0-next.313
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/README.md +80 -88
- package/lib/browser/start-glsp-server-in-worker.d.ts +43 -42
- package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -1
- package/lib/browser/start-glsp-server-in-worker.js +62 -13
- package/lib/browser/start-glsp-server-in-worker.js.map +1 -1
- package/lib/command/diagram-edit-dropped-error.d.ts +31 -0
- package/lib/command/diagram-edit-dropped-error.d.ts.map +1 -0
- package/lib/command/diagram-edit-dropped-error.js +48 -0
- package/lib/command/diagram-edit-dropped-error.js.map +1 -0
- package/lib/command/hydranium-glsp-command-stack.d.ts +33 -0
- package/lib/command/hydranium-glsp-command-stack.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-command-stack.js +63 -0
- package/lib/command/hydranium-glsp-command-stack.js.map +1 -0
- package/lib/command/hydranium-glsp-operation-action-handler.d.ts +64 -0
- package/lib/command/hydranium-glsp-operation-action-handler.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-operation-action-handler.js +142 -0
- package/lib/command/hydranium-glsp-operation-action-handler.js.map +1 -0
- package/lib/command/hydranium-glsp-operation-command.d.ts +250 -0
- package/lib/command/hydranium-glsp-operation-command.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-operation-command.js +491 -0
- package/lib/command/hydranium-glsp-operation-command.js.map +1 -0
- package/lib/command/hydranium-glsp-recording-command.d.ts +26 -87
- package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -1
- package/lib/command/hydranium-glsp-recording-command.js +40 -117
- package/lib/command/hydranium-glsp-recording-command.js.map +1 -1
- package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts +23 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.js +41 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.js.map +1 -0
- package/lib/command/index.d.ts +5 -0
- package/lib/command/index.d.ts.map +1 -1
- package/lib/command/index.js +5 -0
- package/lib/command/index.js.map +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts.map +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js +2 -2
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js.map +1 -1
- package/lib/dispatcher/server-action-dispatcher.d.ts +25 -24
- package/lib/dispatcher/server-action-dispatcher.d.ts.map +1 -1
- package/lib/dispatcher/server-action-dispatcher.js +38 -26
- package/lib/dispatcher/server-action-dispatcher.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +35 -59
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +60 -71
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -1
- package/lib/launcher/drop-notifications-to-gone-client.d.ts +23 -0
- package/lib/launcher/drop-notifications-to-gone-client.d.ts.map +1 -0
- package/lib/launcher/drop-notifications-to-gone-client.js +43 -0
- package/lib/launcher/drop-notifications-to-gone-client.js.map +1 -0
- package/lib/launcher/glsp-app-module.d.ts +1 -2
- package/lib/launcher/glsp-app-module.d.ts.map +1 -1
- package/lib/launcher/glsp-app-module.js.map +1 -1
- package/lib/launcher/glsp-connection-logger.d.ts +19 -0
- package/lib/launcher/glsp-connection-logger.d.ts.map +1 -0
- package/lib/launcher/glsp-connection-logger.js +24 -0
- package/lib/launcher/glsp-connection-logger.js.map +1 -0
- package/lib/launcher/glsp-server-overrides.d.ts +10 -8
- package/lib/launcher/glsp-server-overrides.d.ts.map +1 -1
- package/lib/launcher/glsp-server-overrides.js +25 -11
- package/lib/launcher/glsp-server-overrides.js.map +1 -1
- package/lib/launcher/hydranium-glsp-server.d.ts +60 -15
- package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -1
- package/lib/launcher/hydranium-glsp-server.js +145 -22
- package/lib/launcher/hydranium-glsp-server.js.map +1 -1
- package/lib/launcher/index.d.ts +2 -0
- package/lib/launcher/index.d.ts.map +1 -1
- package/lib/launcher/index.js +5 -4
- package/lib/launcher/index.js.map +1 -1
- package/lib/logging/glsp-client-logger.d.ts +10 -6
- package/lib/logging/glsp-client-logger.d.ts.map +1 -1
- package/lib/logging/glsp-client-logger.js +14 -7
- package/lib/logging/glsp-client-logger.js.map +1 -1
- package/lib/messages/index.d.ts +11 -6
- package/lib/messages/index.d.ts.map +1 -1
- package/lib/messages/index.js +11 -6
- package/lib/messages/index.js.map +1 -1
- package/lib/node/start-glsp-server.d.ts +50 -15
- package/lib/node/start-glsp-server.d.ts.map +1 -1
- package/lib/node/start-glsp-server.js +97 -30
- package/lib/node/start-glsp-server.js.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.d.ts +326 -102
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.js +529 -139
- package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
- package/lib/state/diagram-status.d.ts +38 -0
- package/lib/state/diagram-status.d.ts.map +1 -0
- package/lib/state/diagram-status.js +27 -0
- package/lib/state/diagram-status.js.map +1 -0
- package/lib/state/full-text-hydranium-glsp-state.d.ts +22 -9
- package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.js +45 -18
- package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
- package/lib/state/hydranium-glsp-index.d.ts +40 -29
- package/lib/state/hydranium-glsp-index.d.ts.map +1 -1
- package/lib/state/hydranium-glsp-index.js +82 -45
- package/lib/state/hydranium-glsp-index.js.map +1 -1
- package/lib/state/hydranium-shared-core-services.d.ts +16 -29
- package/lib/state/hydranium-shared-core-services.d.ts.map +1 -1
- package/lib/state/hydranium-shared-core-services.js +15 -28
- package/lib/state/hydranium-shared-core-services.js.map +1 -1
- package/lib/state/index.d.ts +1 -0
- package/lib/state/index.d.ts.map +1 -1
- package/lib/state/index.js +1 -0
- package/lib/state/index.js.map +1 -1
- package/lib/state/model-ready-timeout-error.d.ts.map +1 -1
- package/lib/state/model-ready-timeout-error.js.map +1 -1
- package/lib/state/reconcile-source-model-write.d.ts +13 -27
- package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
- package/lib/state/reconcile-source-model-write.js +44 -32
- package/lib/state/reconcile-source-model-write.js.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.d.ts +125 -69
- package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.js +242 -100
- package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +36 -28
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js +59 -42
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
- package/lib/status/diagram-status-reporter.d.ts +44 -0
- package/lib/status/diagram-status-reporter.d.ts.map +1 -0
- package/lib/status/diagram-status-reporter.js +88 -0
- package/lib/status/diagram-status-reporter.js.map +1 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.d.ts +27 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.d.ts.map +1 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.js +52 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.js.map +1 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts +27 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts.map +1 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js +53 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js.map +1 -0
- package/lib/status/index.d.ts +12 -0
- package/lib/status/index.d.ts.map +1 -0
- package/lib/status/index.js +12 -0
- package/lib/status/index.js.map +1 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts +30 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts.map +1 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.js +47 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.js.map +1 -0
- package/lib/storage/hydranium-glsp-storage.d.ts +324 -143
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
- package/lib/storage/hydranium-glsp-storage.js +569 -213
- package/lib/storage/hydranium-glsp-storage.js.map +1 -1
- package/lib/storage/index.d.ts +2 -1
- package/lib/storage/index.d.ts.map +1 -1
- package/lib/storage/index.js +2 -1
- package/lib/storage/index.js.map +1 -1
- package/lib/storage/save-delivery-policy.d.ts +51 -0
- package/lib/storage/save-delivery-policy.d.ts.map +1 -0
- package/lib/storage/{save-conflict-policy.js → save-delivery-policy.js} +7 -8
- package/lib/storage/save-delivery-policy.js.map +1 -0
- package/lib/submission/hydranium-glsp-submission-handler.d.ts +44 -4
- package/lib/submission/hydranium-glsp-submission-handler.d.ts.map +1 -1
- package/lib/submission/hydranium-glsp-submission-handler.js +82 -6
- package/lib/submission/hydranium-glsp-submission-handler.js.map +1 -1
- package/lib/testing/glsp-harness.d.ts +34 -7
- package/lib/testing/glsp-harness.d.ts.map +1 -1
- package/lib/testing/glsp-harness.js +47 -19
- package/lib/testing/glsp-harness.js.map +1 -1
- package/lib/validation/diagnostic-markers.d.ts +3 -3
- package/lib/validation/diagnostic-markers.d.ts.map +1 -1
- package/lib/validation/diagnostic-markers.js +1 -1
- package/lib/validation/diagnostic-markers.js.map +1 -1
- package/lib/validation/hydranium-glsp-model-validator.d.ts +1 -2
- package/lib/validation/hydranium-glsp-model-validator.d.ts.map +1 -1
- package/lib/validation/hydranium-glsp-model-validator.js +1 -1
- package/lib/validation/hydranium-glsp-model-validator.js.map +1 -1
- package/package.json +31 -52
- package/src/browser/start-glsp-server-in-worker.ts +79 -51
- package/src/command/diagram-edit-dropped-error.ts +51 -0
- package/src/command/hydranium-glsp-command-stack.ts +47 -0
- package/src/command/hydranium-glsp-operation-action-handler.ts +141 -0
- package/src/command/hydranium-glsp-operation-command.ts +602 -0
- package/src/command/hydranium-glsp-recording-command.ts +40 -121
- package/src/command/hydranium-glsp-undo-redo-action-handler.ts +29 -0
- package/src/command/index.ts +10 -0
- package/src/computed-bounds/hydranium-glsp-computed-bounds-action-handler.ts +2 -2
- package/src/dispatcher/server-action-dispatcher.ts +39 -31
- package/src/index.ts +1 -0
- package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +79 -91
- package/src/launcher/drop-notifications-to-gone-client.ts +47 -0
- package/src/launcher/glsp-app-module.ts +1 -2
- package/src/launcher/glsp-connection-logger.ts +27 -0
- package/src/launcher/glsp-server-overrides.ts +27 -11
- package/src/launcher/hydranium-glsp-server.ts +153 -23
- package/src/launcher/index.ts +5 -4
- package/src/logging/glsp-client-logger.ts +16 -9
- package/src/messages/index.ts +16 -6
- package/src/node/start-glsp-server.ts +133 -40
- package/src/state/abstract-hydranium-glsp-state.ts +609 -154
- package/src/state/diagram-status.ts +41 -0
- package/src/state/full-text-hydranium-glsp-state.ts +46 -18
- package/src/state/hydranium-glsp-index.ts +85 -41
- package/src/state/hydranium-shared-core-services.ts +16 -29
- package/src/state/index.ts +1 -0
- package/src/state/reconcile-source-model-write.ts +51 -53
- package/src/state/reconciling-multi-document-glsp-state.ts +276 -102
- package/src/state/reconciling-transfer-hydranium-glsp-state.ts +67 -45
- package/src/status/diagram-status-reporter.ts +106 -0
- package/src/status/hydranium-glsp-request-model-action-handler.ts +43 -0
- package/src/status/hydranium-glsp-set-edit-mode-action-handler.ts +38 -0
- package/src/status/index.ts +12 -0
- package/src/storage/hydranium-glsp-request-save-model-action-handler.ts +40 -0
- package/src/storage/hydranium-glsp-storage.ts +647 -223
- package/src/storage/index.ts +2 -1
- package/src/storage/save-delivery-policy.ts +49 -0
- package/src/submission/hydranium-glsp-submission-handler.ts +91 -5
- package/src/testing/glsp-harness.ts +67 -25
- package/src/validation/diagnostic-markers.ts +3 -3
- package/src/validation/hydranium-glsp-model-validator.ts +4 -5
- package/lib/storage/save-conflict-policy.d.ts +0 -56
- package/lib/storage/save-conflict-policy.d.ts.map +0 -1
- package/lib/storage/save-conflict-policy.js.map +0 -1
- package/src/storage/save-conflict-policy.ts +0 -52
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import type { ProposalString, SeverityLevel } from '@eclipse-glsp/server';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Why a diagram is in a state its client has to show: the key an entry is set
|
|
14
|
+
* and withdrawn under in {@link AbstractHydraniumGlspState.setStatus}.
|
|
15
|
+
*
|
|
16
|
+
* Open, as GLSP's `EditMode` is: an adopter keys its own statuses with any
|
|
17
|
+
* other string. Each writer withdraws only its own key, so no writer clears a
|
|
18
|
+
* status another one still holds.
|
|
19
|
+
*/
|
|
20
|
+
export const DiagramStatus = {
|
|
21
|
+
/** The document does not parse, so the canvas refuses edits until it does. */
|
|
22
|
+
PARSE_ERROR: 'parse-error',
|
|
23
|
+
/** Live validation of the model is running. */
|
|
24
|
+
VALIDATION: 'validation',
|
|
25
|
+
/** The source model is loading. */
|
|
26
|
+
MODEL_LOAD: 'model-load',
|
|
27
|
+
/** The client asked for the diagram to be read-only. */
|
|
28
|
+
CLIENT_REQUEST: 'client-request'
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export type DiagramStatus = ProposalString<(typeof DiagramStatus)[keyof typeof DiagramStatus]>;
|
|
32
|
+
|
|
33
|
+
/** What an active {@link DiagramStatus} does to the diagram. */
|
|
34
|
+
export interface DiagramStatusEntry {
|
|
35
|
+
/** Shown in the client's status overlay; omitted when the status has nothing to say. */
|
|
36
|
+
readonly message?: string;
|
|
37
|
+
/** Picks which message is shown when several statuses carry one; the most recently set wins a tie. */
|
|
38
|
+
readonly severity?: Exclude<SeverityLevel, 'NONE' | 'OK'>;
|
|
39
|
+
/** Blocks editing while the status is active. */
|
|
40
|
+
readonly readonly?: boolean;
|
|
41
|
+
}
|
|
@@ -11,7 +11,7 @@ import { type JsonModelState } from '@eclipse-glsp/server';
|
|
|
11
11
|
import { injectable } from 'inversify';
|
|
12
12
|
import { type AstNode } from '@hydranium/langium';
|
|
13
13
|
import { URI } from '@hydranium/langium';
|
|
14
|
-
import { isPromiseLike, type MaybePromise } from '@hydranium/protocol';
|
|
14
|
+
import { type BaseVersion, isConflictError, isPromiseLike, type MaybePromise } from '@hydranium/protocol';
|
|
15
15
|
import { AbstractHydraniumGlspState } from './abstract-hydranium-glsp-state.js';
|
|
16
16
|
|
|
17
17
|
/** Whole-document-text source model: the serialised text of the source root. */
|
|
@@ -30,15 +30,17 @@ export interface FullTextSourceModel {
|
|
|
30
30
|
* nothing beyond narrowing `TRoot` and their index:
|
|
31
31
|
* - {@link sourceModel} serialises the source root through the per-URI
|
|
32
32
|
* `serializer.Serializer` reached via `sharedServices.ServiceRegistry`
|
|
33
|
-
* (so multi-grammar workspaces route to the right serializer)
|
|
34
|
-
* because serialization may be async;
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* re-parsed
|
|
33
|
+
* (so multi-grammar workspaces route to the right serializer), keeping the
|
|
34
|
+
* document's comments. `MaybePromise` because serialization may be async;
|
|
35
|
+
* the sync fast path is preserved.
|
|
36
|
+
* - {@link updateSourceModel} pushes the text back through the diagram's
|
|
37
|
+
* session (`update` accepts a raw text payload) and captures the re-parsed
|
|
38
|
+
* root, gated on the caller's `baseVersion`; it throws without a session.
|
|
38
39
|
*
|
|
39
|
-
* No `
|
|
40
|
+
* No `base` / conflict reconcile: a whole-document model has exactly one
|
|
40
41
|
* field, so every concurrent edit is a same-field collision and undo / redo
|
|
41
|
-
* degrade to drop-on-divergence
|
|
42
|
+
* degrade to drop-on-divergence, unless the text already holds what they
|
|
43
|
+
* write. Adopters that need field-level undo use
|
|
42
44
|
* `ReconcilingTransferHydraniumGlspState` instead.
|
|
43
45
|
*/
|
|
44
46
|
@injectable()
|
|
@@ -46,18 +48,44 @@ export class FullTextHydraniumGlspState<TRoot extends AstNode>
|
|
|
46
48
|
extends AbstractHydraniumGlspState<TRoot, FullTextSourceModel>
|
|
47
49
|
implements JsonModelState<FullTextSourceModel>
|
|
48
50
|
{
|
|
51
|
+
/**
|
|
52
|
+
* The source root serialised, with the comments of the built document the
|
|
53
|
+
* captured root was parsed from re-attached through the language's trivia
|
|
54
|
+
* service, as a structured write's text gets them. The serializer emits
|
|
55
|
+
* none, so without them every write, undo and redo would delete them.
|
|
56
|
+
*/
|
|
49
57
|
get sourceModel(): MaybePromise<FullTextSourceModel> {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
58
|
+
const uri = URI.parse(this._sourceUri);
|
|
59
|
+
const services = this.sharedServices.ServiceRegistry.getServices(uri);
|
|
60
|
+
const trivia = services.trivia?.TriviaService;
|
|
61
|
+
const document = this._sourceRoot.$document;
|
|
62
|
+
const extracted = trivia !== undefined && document !== undefined ? trivia.extract(document) : undefined;
|
|
63
|
+
const withTrivia = (text: string): FullTextSourceModel => ({
|
|
64
|
+
text: trivia !== undefined && extracted !== undefined ? trivia.apply(text, extracted, uri) : text
|
|
65
|
+
});
|
|
66
|
+
const text = services.serializer.Serializer.serializeAst(this.sourceRoot);
|
|
67
|
+
return isPromiseLike(text) ? text.then(withTrivia) : withTrivia(text);
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
/**
|
|
71
|
+
* A write that conflicts while the store already holds `model.text`, as
|
|
72
|
+
* when an undo's side effect brought the text to the undo's result, is
|
|
73
|
+
* taken as applied; any other conflict is thrown.
|
|
74
|
+
*/
|
|
75
|
+
async updateSourceModel(model: FullTextSourceModel, baseVersion: BaseVersion = this.baseVersion): Promise<void> {
|
|
76
|
+
let document: { root: AstNode };
|
|
77
|
+
try {
|
|
78
|
+
document = await this.requireModelSession().update({ uri: this._sourceUri, model: model.text, baseVersion });
|
|
79
|
+
} catch (error: unknown) {
|
|
80
|
+
if (isConflictError(error) && this.sharedServices.workspace.TextDocuments.get(this._sourceUri)?.getText() === model.text) {
|
|
81
|
+
this.logger.debug(
|
|
82
|
+
`updateSourceModel already applied (model v${error.baseVersion} / text v${error.actualVersion}); the document already holds the diagram edit`
|
|
83
|
+
);
|
|
84
|
+
this.captureBuiltRoot();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
this.captureWrittenRoot(document.root as TRoot);
|
|
62
90
|
}
|
|
63
91
|
}
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import { type GModelElement, GModelIndex } from '@eclipse-glsp/server';
|
|
11
|
-
import { inject, injectable
|
|
11
|
+
import { inject, injectable } from 'inversify';
|
|
12
12
|
import { type AstNode, AstUtils, type URI } from '@hydranium/langium';
|
|
13
|
-
import * as uuid from 'uuid';
|
|
14
13
|
import { type ElementKeyProvider, type ServerLanguageServices, type ServerSharedServices } from '@hydranium/core';
|
|
14
|
+
import { randomUuid } from '@hydranium/protocol';
|
|
15
|
+
import { workingUriOfCopy } from '../command/hydranium-glsp-operation-command.js';
|
|
15
16
|
import { HydraniumTypes } from './hydranium-shared-core-services.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -46,34 +47,24 @@ export class HydraniumGlspIndex extends GModelIndex {
|
|
|
46
47
|
|
|
47
48
|
protected idToSemanticNode = new Map<string, AstNode>();
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The entries {@link indexSemanticElement} added outside the walk, by id.
|
|
52
|
+
* Kept across {@link reindexSemanticElements}, which walks containment
|
|
53
|
+
* only and so cannot rebuild them, and moved with {@link remapSemanticAliases}.
|
|
54
|
+
*/
|
|
55
|
+
protected semanticAliases = new Map<string, AstNode>();
|
|
56
|
+
|
|
57
|
+
/** Whether {@link reindexSemanticElements} is walking, so an entry it adds is not an alias. */
|
|
58
|
+
protected walking = false;
|
|
59
|
+
|
|
49
60
|
/**
|
|
50
61
|
* The language services of the diagram's OWN document, captured from the
|
|
51
62
|
* source root in {@link indexSourceRoot}. Used only as the fallback for a
|
|
52
63
|
* node that routes nowhere — a synthetic or freshly-built node with no
|
|
53
64
|
* `$document` yet, which a GModel factory legitimately hands us mid-edit.
|
|
54
|
-
*
|
|
55
|
-
* Captured rather than injected because it is the language the loaded
|
|
56
|
-
* document ACTUALLY routes to, which is not necessarily the one the diagram
|
|
57
|
-
* module declared — precisely the drift
|
|
58
|
-
* `AbstractHydraniumGlspState.checkDeclaredLanguage` warns about. (Not
|
|
59
|
-
* a cost argument: {@link HydraniumTypes}.DiagramLanguage is bound
|
|
60
|
-
* `inSingletonScope`, so injecting it would resolve once per session too.)
|
|
61
65
|
*/
|
|
62
66
|
protected diagramLanguage?: ServerLanguageServices;
|
|
63
67
|
|
|
64
|
-
/**
|
|
65
|
-
* The grammar the diagram module declared, as the last resort behind
|
|
66
|
-
* {@link diagramLanguage}.
|
|
67
|
-
*
|
|
68
|
-
* Order-independent where the captured one is not: GLSP builds every
|
|
69
|
-
* operation handler at `InitializeClientSession`, so a document-less node
|
|
70
|
-
* can reach {@link createId} before the first {@link indexSourceRoot} — and
|
|
71
|
-
* with no language at all that mints an unstable `fallback_<uuid>` where a
|
|
72
|
-
* stable positional key was available. `@optional()` so a harness that binds
|
|
73
|
-
* no diagram module still resolves.
|
|
74
|
-
*/
|
|
75
|
-
@inject(HydraniumTypes.DiagramLanguage) @optional() protected readonly declaredLanguage?: ServerLanguageServices;
|
|
76
|
-
|
|
77
68
|
/**
|
|
78
69
|
* Reverse map: the *stable id* of the AST element a GModel element
|
|
79
70
|
* represents → the GModel id(s) that render it. Multi-valued because one
|
|
@@ -140,8 +131,8 @@ export class HydraniumGlspIndex extends GModelIndex {
|
|
|
140
131
|
|
|
141
132
|
/**
|
|
142
133
|
* The {@link ElementKeyProvider} of the language owning `node`'s document,
|
|
143
|
-
* falling back to the language the diagram document routes to
|
|
144
|
-
*
|
|
134
|
+
* falling back to the language the diagram document routes to for a node
|
|
135
|
+
* that routes nowhere.
|
|
145
136
|
*
|
|
146
137
|
* **Resolved per node on the cross-document entry points.** This index is
|
|
147
138
|
* deliberately cross-document — {@link registerElementId} and
|
|
@@ -155,13 +146,24 @@ export class HydraniumGlspIndex extends GModelIndex {
|
|
|
155
146
|
* {@link indexSourceRoot} needs no per-node lookup and does not pay for one.
|
|
156
147
|
*/
|
|
157
148
|
protected elementKeyProviderFor(node?: AstNode): ElementKeyProvider | undefined {
|
|
158
|
-
|
|
159
|
-
|
|
149
|
+
return this.languageServicesFor(node)?.references.ElementKeyProvider;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The language services owning `node`, falling back to the diagram's
|
|
154
|
+
* language for a node that routes nowhere. A per-language lookup an index
|
|
155
|
+
* subclass adds goes through this, so an operation's copy of a node from
|
|
156
|
+
* another document reaches that document's language, as its built node does.
|
|
157
|
+
*/
|
|
158
|
+
languageServicesFor(node?: AstNode): ServerLanguageServices | undefined {
|
|
159
|
+
// A copy node has no `$document`; it routes by its working root's URI.
|
|
160
|
+
const route = node === undefined ? undefined : workingUriOfCopy(node);
|
|
161
|
+
return this.sharedServices.ServiceRegistry.getServicesFor(route ?? node) ?? this.diagramLanguage;
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
/** Return the GModel id assigned to `node`, falling back to a fresh `fallback_<uuid>`. */
|
|
163
165
|
createId(node?: AstNode): string {
|
|
164
|
-
return this.findId(node, () => 'fallback_' +
|
|
166
|
+
return this.findId(node, () => 'fallback_' + randomUuid());
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
/**
|
|
@@ -186,14 +188,31 @@ export class HydraniumGlspIndex extends GModelIndex {
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
+
* Index `root` as a new source root: forget what {@link registerElementId}
|
|
192
|
+
* and {@link indexSemanticElement} recorded, seed the diagram document as
|
|
193
|
+
* rendered, and index the nodes through {@link reindexSemanticElements}.
|
|
194
|
+
*/
|
|
195
|
+
indexSourceRoot(root: AstNode, uri?: URI | string): void {
|
|
196
|
+
this.semanticAliases.clear();
|
|
197
|
+
this.elementToIds.clear();
|
|
198
|
+
this.renderedDocUris.clear();
|
|
199
|
+
this.addRenderedDocUri(root);
|
|
200
|
+
this.reindexSemanticElements(root, uri);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Index the nodes of `root` by id, keeping what {@link registerElementId}
|
|
205
|
+
* and, outside the walk, {@link indexSemanticElement} recorded. For a root
|
|
206
|
+
* carrying the same ids as the one indexed, such as a working copy: those
|
|
207
|
+
* are made only when the GModel is built, and clearing them leaves
|
|
208
|
+
* diagnostics with no marker and an alias resolving to nothing until the
|
|
209
|
+
* next build. An alias still names its node; move it with
|
|
210
|
+
* {@link remapSemanticAliases}.
|
|
191
211
|
*
|
|
192
|
-
* `uri` is the diagram document's URI
|
|
193
|
-
* `
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* either way since it is what the caller declared it was loading.
|
|
212
|
+
* `uri` is the diagram document's URI and is used only to resolve the
|
|
213
|
+
* diagram's language when `root` carries no `$document` — always the case
|
|
214
|
+
* for a synthesised root or a working copy, and the more authoritative
|
|
215
|
+
* signal either way since it is what the caller declared it was loading.
|
|
197
216
|
*
|
|
198
217
|
* The walk resolves the key provider ONCE and threads it: `streamAllContents`
|
|
199
218
|
* is containment-only, so every node it yields has `root` as its root node
|
|
@@ -202,15 +221,37 @@ export class HydraniumGlspIndex extends GModelIndex {
|
|
|
202
221
|
* languageId, file name, extension — for an answer already in hand, on every
|
|
203
222
|
* diagram open and every source-root refresh.
|
|
204
223
|
*/
|
|
205
|
-
|
|
224
|
+
reindexSemanticElements(root: AstNode, uri?: URI | string): void {
|
|
206
225
|
this.idToSemanticNode.clear();
|
|
207
|
-
this.elementToIds.clear();
|
|
208
|
-
this.renderedDocUris.clear();
|
|
209
226
|
const registry = this.sharedServices.ServiceRegistry;
|
|
210
227
|
this.diagramLanguage = registry.getServicesFor(root) ?? registry.getServicesFor(uri);
|
|
211
|
-
this.addRenderedDocUri(root);
|
|
212
228
|
const keyProvider = this.elementKeyProviderFor(root);
|
|
213
|
-
|
|
229
|
+
this.walking = true;
|
|
230
|
+
try {
|
|
231
|
+
AstUtils.streamAllContents(root).forEach(node => this.indexAstNode(node, keyProvider));
|
|
232
|
+
} finally {
|
|
233
|
+
this.walking = false;
|
|
234
|
+
}
|
|
235
|
+
for (const [id, node] of this.semanticAliases) {
|
|
236
|
+
this.idToSemanticNode.set(id, node);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Point every alias {@link indexSemanticElement} added at `map(node)`, or
|
|
242
|
+
* drop it where that is `undefined`: the node it named is gone.
|
|
243
|
+
*/
|
|
244
|
+
remapSemanticAliases(map: (node: AstNode) => AstNode | undefined): void {
|
|
245
|
+
for (const [id, node] of [...this.semanticAliases]) {
|
|
246
|
+
const mapped = map(node);
|
|
247
|
+
if (mapped === undefined) {
|
|
248
|
+
this.semanticAliases.delete(id);
|
|
249
|
+
this.idToSemanticNode.delete(id);
|
|
250
|
+
} else {
|
|
251
|
+
this.semanticAliases.set(id, mapped);
|
|
252
|
+
this.idToSemanticNode.set(id, mapped);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
214
255
|
}
|
|
215
256
|
|
|
216
257
|
/**
|
|
@@ -242,7 +283,7 @@ export class HydraniumGlspIndex extends GModelIndex {
|
|
|
242
283
|
|
|
243
284
|
/** Record `node`'s document uri (if it has one) as contributing rendered elements. */
|
|
244
285
|
protected addRenderedDocUri(node: AstNode): void {
|
|
245
|
-
const uri = AstUtils.findRootNode(node).$document?.uri.toString();
|
|
286
|
+
const uri = AstUtils.findRootNode(node).$document?.uri.toString() ?? workingUriOfCopy(node);
|
|
246
287
|
if (uri !== undefined) {
|
|
247
288
|
this.renderedDocUris.add(uri);
|
|
248
289
|
}
|
|
@@ -266,6 +307,9 @@ export class HydraniumGlspIndex extends GModelIndex {
|
|
|
266
307
|
/** Insert an AST node under `id` directly — for adopters that need to seed entries outside the standard walk. */
|
|
267
308
|
indexSemanticElement<T extends AstNode>(id: string, element: T): void {
|
|
268
309
|
this.idToSemanticNode.set(id, element);
|
|
310
|
+
if (!this.walking) {
|
|
311
|
+
this.semanticAliases.set(id, element);
|
|
312
|
+
}
|
|
269
313
|
}
|
|
270
314
|
|
|
271
315
|
/** Look up the AST node previously indexed under `id`. */
|
|
@@ -329,7 +373,7 @@ export class HydraniumGlspIndex extends GModelIndex {
|
|
|
329
373
|
*/
|
|
330
374
|
protected override doIndex(element: GModelElement): void {
|
|
331
375
|
if (this.idToElement.has(element.id)) {
|
|
332
|
-
this.sharedServices.Logger.for('Index').
|
|
376
|
+
this.sharedServices.Logger.for('Index').warn('Skipping duplicate element id in graph: ' + element.id);
|
|
333
377
|
return;
|
|
334
378
|
}
|
|
335
379
|
super.doIndex(element);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
10
|
+
import type { ServerSharedServices } from '@hydranium/core';
|
|
11
11
|
import type { ConflictResolver, Tracer } from '@hydranium/protocol';
|
|
12
12
|
import { serviceIdentifier } from '../util/service-identifier.js';
|
|
13
13
|
|
|
@@ -19,37 +19,23 @@ import { serviceIdentifier } from '../util/service-identifier.js';
|
|
|
19
19
|
* framework hands around via plain constructor arguments.
|
|
20
20
|
*
|
|
21
21
|
* Every token here is a service whose value is **fixed for the container tier
|
|
22
|
-
* that binds it
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* that binds it**: the shared services and the conflict policy for the
|
|
23
|
+
* process, the tracer for each request. That is the whole membership rule, and
|
|
24
|
+
* it is why the registry is short: see below for what deliberately does NOT
|
|
25
|
+
* get a token.
|
|
26
26
|
*
|
|
27
27
|
* Grouped under one object (rather than top-level `Hydranium<Role>` consts)
|
|
28
28
|
* so the token identity is decoupled from the implementation class name.
|
|
29
29
|
* Mirrors GLSP's own `TYPES` registry idiom.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* **Split by tier, because GLSP has no language tier.** GLSP's containers are
|
|
36
|
-
* app (per process) -> server (per connection) -> session (per open diagram);
|
|
37
|
-
* Langium's are shared (per process) -> language (per grammar, routed by URI).
|
|
38
|
-
* Only the outermost pair line up. A per-language service therefore CANNOT be
|
|
39
|
-
* bound at the app tier — that container is built before any document exists,
|
|
40
|
-
* so it can only ever hold one grammar's services. The tokens below are split
|
|
41
|
-
* accordingly:
|
|
31
|
+
* `SharedCoreServices` and `ConflictResolver` are bound by
|
|
32
|
+
* `HydraniumGlspAppModule`, `Tracer` by the launchers. Inject each with the
|
|
33
|
+
* real service type as the field type.
|
|
42
34
|
*
|
|
43
35
|
* - **SharedCoreServices** — the framework's Langium-style shared services
|
|
44
36
|
* tree ({@link ServerSharedServices}); the services-tree root that state /
|
|
45
37
|
* index classes inject. Bound once at the app tier. Carries
|
|
46
38
|
* `ServiceRegistry`, which is how everything below reaches a language.
|
|
47
|
-
* - **DiagramLanguage** — the {@link ServerLanguageServices} of the grammar a
|
|
48
|
-
* diagram type edits, bound at the SESSION tier by
|
|
49
|
-
* `AbstractHydraniumGlspDiagramModule` from its declared `declareLanguage()`.
|
|
50
|
-
* Static (a diagram type has one grammar), so it is safe for the eagerly
|
|
51
|
-
* constructed handlers GLSP builds at `InitializeClientSession`. Read it
|
|
52
|
-
* through `AbstractHydraniumGlspState.diagramLanguage`.
|
|
53
39
|
* - **ConflictResolver** — the policy the GLSP state consults when a write
|
|
54
40
|
* (forward-write, undo, redo, save) races a foreign edit.
|
|
55
41
|
* `HydraniumGlspAppModule` binds it to `options.conflictResolver`
|
|
@@ -62,8 +48,14 @@ import { serviceIdentifier } from '../util/service-identifier.js';
|
|
|
62
48
|
* component — no manual `for(...)`. The non-injectable recording command,
|
|
63
49
|
* which can't inject, borrows the state's `tracer` getter instead.
|
|
64
50
|
*
|
|
65
|
-
* **No per-language
|
|
66
|
-
*
|
|
51
|
+
* **No language and no per-language service has a token, because GLSP has no
|
|
52
|
+
* language tier.** GLSP's containers are app (per process) -> server (per
|
|
53
|
+
* connection) -> session (per open diagram); Langium's are shared (per
|
|
54
|
+
* process) -> language (per grammar, routed by URI). Only the outermost pair
|
|
55
|
+
* line up. The app container is built before any document exists, and a
|
|
56
|
+
* session's handlers before its document is loaded, so neither knows the
|
|
57
|
+
* diagram's language: the one its loaded document routes to. Nor is there an
|
|
58
|
+
* `ElementKeyProvider`, `NameProvider`, `ScopeProvider` or
|
|
67
59
|
* `CandidateProvider` symbol, because every one of them is keyed by a document
|
|
68
60
|
* that the container cannot know: the target node's, for naming and keying;
|
|
69
61
|
* the one a reference is WRITTEN in, for scope and candidates. A GLSP
|
|
@@ -79,14 +71,9 @@ import { serviceIdentifier } from '../util/service-identifier.js';
|
|
|
79
71
|
* reference written on the canvas, `modelState.languageServicesFor(node)` for
|
|
80
72
|
* anything reached through a reference. `ServiceRegistry.getServicesFor(target)`
|
|
81
73
|
* serves the same purpose where no model state is at hand.
|
|
82
|
-
*
|
|
83
|
-
* The app-tier tokens are bound by `HydraniumGlspAppModule`, the
|
|
84
|
-
* session-tier one by `AbstractHydraniumGlspDiagramModule`. A multi-grammar
|
|
85
|
-
* adopter declares a language per diagram module and needs nothing else.
|
|
86
74
|
*/
|
|
87
75
|
export const HydraniumTypes = {
|
|
88
76
|
SharedCoreServices: serviceIdentifier<ServerSharedServices>('HydraniumSharedCoreServices'),
|
|
89
|
-
DiagramLanguage: serviceIdentifier<ServerLanguageServices>('HydraniumDiagramLanguage'),
|
|
90
77
|
ConflictResolver: serviceIdentifier<ConflictResolver>('HydraniumConflictResolver'),
|
|
91
78
|
Tracer: serviceIdentifier<Tracer>('HydraniumTracer')
|
|
92
79
|
} as const;
|
package/src/state/index.ts
CHANGED
|
@@ -7,71 +7,69 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { type
|
|
10
|
+
import { type BaseVersion, type Logger, type ReconcileWriteHooks, reconcileWrite } from '@hydranium/protocol';
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
* a plain record rather than a base class: the states that use it differ in what
|
|
15
|
-
* "persist" and "project" mean but not at all in how a conflict is handled, and
|
|
16
|
-
* pulling the orchestration out is what keeps that second half from being
|
|
17
|
-
* written once per state.
|
|
18
|
-
*/
|
|
19
|
-
export interface SourceModelWriteHooks<TModel> {
|
|
20
|
-
/**
|
|
21
|
-
* Write `model` and capture the resulting state. Called with a `baseVersion`
|
|
22
|
-
* on the first attempt (opting into the `ConflictError` gate) and without one
|
|
23
|
-
* on the merged / forced retries, where the point is to land the write.
|
|
24
|
-
*/
|
|
25
|
-
persist(model: TModel, baseVersion?: number): Promise<void>;
|
|
26
|
-
/** Current server-side projection, or `undefined` when unavailable. */
|
|
27
|
-
refetch(): Promise<TModel | undefined>;
|
|
28
|
-
/** Last in-sync projection the user's intent is measured against. */
|
|
29
|
-
readonly baseline: TModel | undefined;
|
|
30
|
-
readonly conflictResolver: ConflictResolver;
|
|
12
|
+
/** The I/O and policy a {@link reconcileSourceModelWrite} call needs. */
|
|
13
|
+
export interface SourceModelWriteHooks<TModel> extends Omit<ReconcileWriteHooks<TModel>, 'onUnavailable'> {
|
|
31
14
|
readonly logger: Logger;
|
|
32
15
|
/** Invoked when the edit is dropped, so the caller can resync its own state. */
|
|
33
16
|
onConflictDropped(): void;
|
|
17
|
+
/** Invoked when the document already held the edit, so the caller can take its built root. */
|
|
18
|
+
onAlreadyApplied(): void;
|
|
34
19
|
}
|
|
35
20
|
|
|
36
21
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
22
|
+
* {@link reconcileWrite} for a diagram edit: logs the outcome it ends, and
|
|
23
|
+
* resyncs through `onConflictDropped` when the edit is dropped, through
|
|
24
|
+
* `onAlreadyApplied` when the document already held it.
|
|
39
25
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
26
|
+
* A conflict whose refetch is unavailable, the current text not readable to
|
|
27
|
+
* reconcile against, throws the `ConflictError` the write raised: forcing the
|
|
28
|
+
* edit would overwrite text the diagram never saw. An operation then rolls
|
|
29
|
+
* back and an undo or redo fails, as for any write that throws.
|
|
44
30
|
*/
|
|
45
31
|
export async function reconcileSourceModelWrite<TModel extends object>(
|
|
46
32
|
model: TModel,
|
|
47
|
-
|
|
33
|
+
baseVersion: BaseVersion,
|
|
48
34
|
hooks: SourceModelWriteHooks<TModel>
|
|
49
35
|
): Promise<void> {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
36
|
+
const outcome = await reconcileWrite(model, baseVersion, {
|
|
37
|
+
persist: (candidate, candidateBaseVersion) => hooks.persist(candidate, candidateBaseVersion),
|
|
38
|
+
refetch: () => hooks.refetch(),
|
|
39
|
+
base: hooks.base,
|
|
40
|
+
conflictResolver: hooks.conflictResolver,
|
|
41
|
+
maxWrites: hooks.maxWrites
|
|
42
|
+
});
|
|
43
|
+
if (outcome.status === 'persisted') {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const { conflict, writes } = outcome;
|
|
47
|
+
const versions = `model v${conflict.baseVersion} / text v${conflict.actualVersion}`;
|
|
48
|
+
if (outcome.status === 'unavailable') {
|
|
49
|
+
// The conflict carries both versions to whoever ends the failure, which logs it; why it was not reconciled is diagnosis.
|
|
50
|
+
hooks.logger.debug(`updateSourceModel refetch unavailable (${versions}); the diagram edit fails rather than overwrite unread text`);
|
|
51
|
+
throw conflict;
|
|
52
|
+
}
|
|
53
|
+
switch (outcome.status) {
|
|
54
|
+
case 'merged':
|
|
55
|
+
hooks.logger.debug(`updateSourceModel merged (${versions}); landed on write ${writes}`);
|
|
56
|
+
return;
|
|
57
|
+
case 'already-applied':
|
|
58
|
+
hooks.logger.debug(`updateSourceModel already applied (${versions}); the document already holds the diagram edit`);
|
|
59
|
+
hooks.onAlreadyApplied();
|
|
60
|
+
return;
|
|
61
|
+
case 'unchanged':
|
|
62
|
+
hooks.logger.debug(`updateSourceModel no-op (${versions}); the diagram is behind, its update catches it up`);
|
|
63
|
+
return;
|
|
64
|
+
case 'conflict':
|
|
65
|
+
hooks.logger.warn(`updateSourceModel conflict (${versions}); dropping the diagram edit — a foreign writer changed the same field`);
|
|
66
|
+
hooks.onConflictDropped();
|
|
67
|
+
return;
|
|
68
|
+
case 'dropped':
|
|
69
|
+
hooks.logger.warn(
|
|
70
|
+
`updateSourceModel still conflicting after ${writes} ${writes === 1 ? 'write' : 'writes'} (${versions}); dropping the diagram edit`
|
|
71
|
+
);
|
|
72
|
+
hooks.onConflictDropped();
|
|
73
|
+
return;
|
|
76
74
|
}
|
|
77
75
|
}
|