@hydranium/glsp-server 1.0.0-next.28 → 1.0.0-next.281
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 +77 -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 +30 -8
- package/lib/node/start-glsp-server.d.ts.map +1 -1
- package/lib/node/start-glsp-server.js +49 -10
- package/lib/node/start-glsp-server.js.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.d.ts +321 -102
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.js +524 -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 +123 -69
- package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.js +240 -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 +322 -143
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
- package/lib/storage/hydranium-glsp-storage.js +563 -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 +46 -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 +29 -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 +60 -14
- package/src/state/abstract-hydranium-glsp-state.ts +604 -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 +274 -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 +639 -223
- package/src/storage/index.ts +2 -1
- package/src/storage/save-delivery-policy.ts +44 -0
- package/src/submission/hydranium-glsp-submission-handler.ts +91 -5
- package/src/testing/glsp-harness.ts +62 -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
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { DefaultModelState, type MaybePromise, SOURCE_URI_ARG } from '@eclipse-glsp/server';
|
|
10
|
+
import { DefaultModelState, EditMode, type MaybePromise, SOURCE_URI_ARG } from '@eclipse-glsp/server';
|
|
11
11
|
import { Emitter, type Event } from 'vscode-jsonrpc';
|
|
12
|
-
import { inject, injectable
|
|
13
|
-
import { type AstNode, DocumentState, URI } from '@hydranium/langium';
|
|
12
|
+
import { inject, injectable } from 'inversify';
|
|
13
|
+
import { type AstNode, AstUtils, DocumentState, isAstNode, type Reference, type ReferenceInfo, URI } from '@hydranium/langium';
|
|
14
14
|
import {
|
|
15
|
+
buildAstNode,
|
|
16
|
+
type ClientSession,
|
|
15
17
|
type HydraniumScopeProvider,
|
|
16
18
|
type LanguageTarget,
|
|
17
19
|
type NameProvider,
|
|
@@ -19,7 +21,21 @@ import {
|
|
|
19
21
|
type ServerLanguageServices,
|
|
20
22
|
type ServerSharedServices
|
|
21
23
|
} from '@hydranium/core';
|
|
22
|
-
import
|
|
24
|
+
import {
|
|
25
|
+
type BaseVersion,
|
|
26
|
+
type ModelVersion,
|
|
27
|
+
type ConflictResolver,
|
|
28
|
+
type Logger,
|
|
29
|
+
type Tracer,
|
|
30
|
+
type TransferElement,
|
|
31
|
+
asModelVersion,
|
|
32
|
+
TIMED_OUT,
|
|
33
|
+
UNRECORDED_VERSION,
|
|
34
|
+
describeError,
|
|
35
|
+
describeErrorWithStack
|
|
36
|
+
} from '@hydranium/protocol';
|
|
37
|
+
import { type OperationTransition, openOperationOf } from '../command/hydranium-glsp-operation-command.js';
|
|
38
|
+
import { type DiagramStatus, type DiagramStatusEntry } from './diagram-status.js';
|
|
23
39
|
import { type HydraniumGlspIndex } from './hydranium-glsp-index.js';
|
|
24
40
|
import { HydraniumTypes } from './hydranium-shared-core-services.js';
|
|
25
41
|
import { ModelReadyTimeoutError } from './model-ready-timeout-error.js';
|
|
@@ -49,7 +65,7 @@ import { ModelReadyTimeoutError } from './model-ready-timeout-error.js';
|
|
|
49
65
|
* override only to change the error shape (richer per-adopter output comes
|
|
50
66
|
* from overriding `wsRelativePath` / `formatBuildStatus`).
|
|
51
67
|
* - logging — see {@link baseTracer}. Adopters wanting a different label
|
|
52
|
-
* override `
|
|
68
|
+
* override `captureSourceRoot` and derive via
|
|
53
69
|
* `this.baseTracer.for('Name').withUri(uri)`.
|
|
54
70
|
*
|
|
55
71
|
* **Lifecycle ordering.** Reading {@link sourceUri} or {@link sourceRoot}
|
|
@@ -58,7 +74,7 @@ import { ModelReadyTimeoutError } from './model-ready-timeout-error.js';
|
|
|
58
74
|
* (typically from the storage `loadSourceModel` flow) before reading them.
|
|
59
75
|
* The {@link logger}/{@link tracer} are the exception: the injected
|
|
60
76
|
* {@link baseTracer} is always available, so the getters fall back to it
|
|
61
|
-
* before {@link
|
|
77
|
+
* before {@link captureSourceRoot} derives the URI-tagged {@link _tracer} —
|
|
62
78
|
* logging is never `undefined`.
|
|
63
79
|
*/
|
|
64
80
|
@injectable()
|
|
@@ -76,9 +92,15 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
76
92
|
*/
|
|
77
93
|
@inject(HydraniumTypes.ConflictResolver) readonly conflictResolver!: ConflictResolver;
|
|
78
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Writes of one diagram edit, the first included, that may conflict before
|
|
97
|
+
* the edit is dropped with a warning; each further write reconciles again.
|
|
98
|
+
*/
|
|
99
|
+
protected readonly maxSourceModelWrites: number = 3;
|
|
100
|
+
|
|
79
101
|
/**
|
|
80
102
|
* Per-class tracer, caller-tagged with this state's runtime subclass name by
|
|
81
|
-
* the `HydraniumTypes.Tracer` binding. {@link
|
|
103
|
+
* the `HydraniumTypes.Tracer` binding. {@link captureSourceRoot} derives the
|
|
82
104
|
* URI-tagged {@link _tracer} from it; the {@link tracer}/{@link logger}
|
|
83
105
|
* getters fall back to it before then, so logging is never `undefined`.
|
|
84
106
|
*/
|
|
@@ -87,12 +109,62 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
87
109
|
/** Narrows {@link DefaultModelState.index} to the framework-extended index type. */
|
|
88
110
|
declare readonly index: HydraniumGlspIndex;
|
|
89
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The client session this diagram works as, registered under `clientId` by
|
|
114
|
+
* the source-model storage's load and ended with the GLSP client session,
|
|
115
|
+
* or by a resume that takes it over. `undefined` before the load, for
|
|
116
|
+
* GLSP's placeholder client, and when another participant held the id, in
|
|
117
|
+
* which case the diagram does not load.
|
|
118
|
+
*
|
|
119
|
+
* Every write of the diagram goes through this session, which writes only a
|
|
120
|
+
* document it has open and opens nothing. A one-shot write to a document
|
|
121
|
+
* outside the write set goes through
|
|
122
|
+
* {@link ClientSession.withOpen}, and a document the diagram brings into
|
|
123
|
+
* existence through {@link ClientSession.create}.
|
|
124
|
+
*/
|
|
125
|
+
modelSession?: ClientSession<AstNode>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* {@link modelSession}, or a throw without one: the diagram never loaded or
|
|
129
|
+
* has ended. Looking `clientId` up through `ModelService.getSession` instead
|
|
130
|
+
* would write as whichever participant holds that id.
|
|
131
|
+
*/
|
|
132
|
+
protected requireModelSession(): ClientSession<AstNode> {
|
|
133
|
+
if (!this.modelSession) {
|
|
134
|
+
throw new Error(`No client session is registered for ${this.clientId}; the diagram cannot write`);
|
|
135
|
+
}
|
|
136
|
+
return this.modelSession;
|
|
137
|
+
}
|
|
138
|
+
|
|
90
139
|
protected _sourceUri!: string;
|
|
140
|
+
/**
|
|
141
|
+
* The language {@link _sourceUri} routed to at the last capture, or
|
|
142
|
+
* `undefined` when it routes nowhere. Captured rather than looked up on
|
|
143
|
+
* each {@link diagramLanguage} read, which a GModel factory may make once
|
|
144
|
+
* per element, because the lookup parses the URI and walks the registry.
|
|
145
|
+
*/
|
|
146
|
+
protected _sourceLanguage?: ServerLanguageServices;
|
|
147
|
+
/**
|
|
148
|
+
* The root the last capture took, which every reader of the document
|
|
149
|
+
* shares. A projection that must see an operation's edits reads
|
|
150
|
+
* {@link sourceRoot}, which is a copy of it during an operation.
|
|
151
|
+
*/
|
|
91
152
|
protected _sourceRoot!: TRoot;
|
|
153
|
+
/** Settles when the last {@link runExclusive} call queued so far has. */
|
|
154
|
+
protected exclusiveTail: Promise<unknown> = Promise.resolve();
|
|
92
155
|
protected _tracer?: Tracer;
|
|
93
|
-
protected
|
|
94
|
-
/**
|
|
95
|
-
|
|
156
|
+
protected _baseVersion: ModelVersion = asModelVersion(0);
|
|
157
|
+
/**
|
|
158
|
+
* The built root each secondary's version was read from, keyed by
|
|
159
|
+
* canonical URI, and during an operation the root of any other document it
|
|
160
|
+
* first reached. An operation copies a secondary from here, so the copy,
|
|
161
|
+
* the base a conflict reconciles from and the version its write is gated
|
|
162
|
+
* on describe one revision; the registry's current root can be a later
|
|
163
|
+
* build's.
|
|
164
|
+
*/
|
|
165
|
+
protected readonly capturedRoots = new Map<string, AstNode>();
|
|
166
|
+
/** Model versions of the secondary write set, keyed by URI. See {@link trackSecondaryDocument}. */
|
|
167
|
+
protected readonly _secondaryVersions = new Map<string, ModelVersion>();
|
|
96
168
|
|
|
97
169
|
/** Backing emitter for {@link onSecondaryUrisChanged}; fired only on a membership change. */
|
|
98
170
|
protected readonly secondaryUrisChangedEmitter = new Emitter<void>();
|
|
@@ -104,29 +176,54 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
104
176
|
protected readyTimeoutMs: number = AbstractHydraniumGlspState.DEFAULT_READY_TIMEOUT_MS;
|
|
105
177
|
|
|
106
178
|
/**
|
|
107
|
-
* Capture a freshly built source root alongside its URI
|
|
108
|
-
*
|
|
109
|
-
*
|
|
179
|
+
* Capture a freshly built source root alongside its URI through
|
|
180
|
+
* {@link captureSourceRoot}, and index the AST so downstream GModel
|
|
181
|
+
* projection can resolve elements back to nodes.
|
|
110
182
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
183
|
+
* Capturing the document already captured keeps what the last GModel build
|
|
184
|
+
* registered in the index, which only the next build makes again, and drops
|
|
185
|
+
* an id added through `indexSemanticElement` whose node a rebuild replaced.
|
|
186
|
+
*
|
|
187
|
+
* Throws during an operation: a capture there swaps the root out from under
|
|
188
|
+
* the nodes a handler resolved, and the operation captures when it ends.
|
|
114
189
|
*/
|
|
115
190
|
setSourceRoot(uri: string, root: TRoot): void {
|
|
191
|
+
if (openOperationOf(this)) {
|
|
192
|
+
throw new Error(`setSourceRoot(${uri}) during an operation: the operation captures the source root when it ends`);
|
|
193
|
+
}
|
|
194
|
+
const recapture = uri === this._sourceUri;
|
|
195
|
+
this.captureSourceRoot(uri, root);
|
|
196
|
+
if (recapture) {
|
|
197
|
+
this.index.reindexSemanticElements(root, uri);
|
|
198
|
+
this.index.remapSemanticAliases(node => (this.isCurrentBuiltNode(node) ? node : undefined));
|
|
199
|
+
} else {
|
|
200
|
+
this.index.indexSourceRoot(root, uri);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Take `root` as the source root and read everything derived from it: the
|
|
206
|
+
* base version, every tracked secondary version, the URI mirrored into the
|
|
207
|
+
* inherited properties map under {@link SOURCE_URI_ARG}, and the
|
|
208
|
+
* URI-labelled logger. Everything but the index, which
|
|
209
|
+
* {@link setSourceRoot} sets. Override to derive more from a capture.
|
|
210
|
+
*/
|
|
211
|
+
protected captureSourceRoot(uri: string, root: TRoot): void {
|
|
116
212
|
this._sourceUri = uri;
|
|
213
|
+
this._sourceLanguage = this.sharedServices.ServiceRegistry.getServicesFor(uri);
|
|
117
214
|
this._sourceRoot = root;
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
//
|
|
215
|
+
// The root's own version: the registry's current root can be a later
|
|
216
|
+
// build's, and a write based on its version passes the gate over edits
|
|
217
|
+
// `root` never saw.
|
|
218
|
+
this._baseVersion = this.sharedServices.workspace.ModelLedger.versionOf(root);
|
|
219
|
+
// Re-read the secondary write set: the write that brought us here
|
|
220
|
+
// advanced their versions too, and a stale base version would gate the
|
|
221
|
+
// NEXT command against a superseded number — reporting a conflict that is
|
|
222
|
+
// really this state's own last write.
|
|
124
223
|
this.refreshSecondaryVersions();
|
|
125
224
|
this.set(SOURCE_URI_ARG, uri);
|
|
126
225
|
this._tracer = this.baseTracer.withUri(uri);
|
|
127
|
-
this.tracer.debug(`Captured source root at doc.version=v${this.
|
|
128
|
-
this.checkDeclaredLanguage(uri);
|
|
129
|
-
this.index.indexSourceRoot(root, uri);
|
|
226
|
+
this.tracer.debug(`Captured source root at doc.version=v${this._baseVersion}`);
|
|
130
227
|
}
|
|
131
228
|
|
|
132
229
|
/**
|
|
@@ -138,34 +235,239 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
138
235
|
return this._sourceUri;
|
|
139
236
|
}
|
|
140
237
|
|
|
238
|
+
/** The root operation handlers edit: during an operation a copy of the built root, outside one the built root. */
|
|
141
239
|
get sourceRoot(): TRoot {
|
|
142
|
-
return this._sourceRoot;
|
|
240
|
+
return openOperationOf(this) && this._sourceRoot !== undefined ? (this.workingRootOf(this._sourceUri) as TRoot) : this._sourceRoot;
|
|
143
241
|
}
|
|
144
242
|
|
|
145
243
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
244
|
+
* Run `run` once every earlier call has settled, thrown or not: the
|
|
245
|
+
* boundary that orders operations, undo and redo, and the storage's capture
|
|
246
|
+
* and render. Code running inside it never calls this again, since the
|
|
247
|
+
* call would wait for the section it runs in.
|
|
248
|
+
*/
|
|
249
|
+
runExclusive<T>(run: () => MaybePromise<T>): Promise<T> {
|
|
250
|
+
const result = this.exclusiveTail.then(() => run());
|
|
251
|
+
this.exclusiveTail = result.catch(() => undefined);
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The root of `uri` that operation handlers edit: during an operation a
|
|
257
|
+
* copy made on the first request and returned for the rest of it, outside
|
|
258
|
+
* one the root itself. The root is the built root of `uri`, or `orElse()`
|
|
259
|
+
* when `uri` has no document; `undefined` when neither gives one.
|
|
148
260
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
261
|
+
* A handler that edits another document than the source reaches its root
|
|
262
|
+
* through this, or the edit lands on the root every reader shares. An id
|
|
263
|
+
* added through `indexSemanticElement` for a node of that root names the
|
|
264
|
+
* node's copy from then on.
|
|
265
|
+
*/
|
|
266
|
+
workingRootOf(uri: string, orElse?: () => AstNode | undefined): AstNode | undefined {
|
|
267
|
+
const key = this.workingKey(uri);
|
|
268
|
+
const primary = this._sourceUri !== undefined && key === this.workingKey(this._sourceUri);
|
|
269
|
+
const operation = openOperationOf(this);
|
|
270
|
+
if (!operation) {
|
|
271
|
+
return primary ? this._sourceRoot : (this.sharedServices.model.ModelService.getDocument(uri)?.parseResult.value ?? orElse?.());
|
|
272
|
+
}
|
|
273
|
+
let built: AstNode | undefined = primary ? this._sourceRoot : this.capturedRoots.get(key);
|
|
274
|
+
if (built === undefined) {
|
|
275
|
+
// First reached during the operation: frozen now, for the rest of it.
|
|
276
|
+
built = this.readBuiltRoot(uri);
|
|
277
|
+
if (built !== undefined) {
|
|
278
|
+
this.capturedRoots.set(key, built);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
built ??= orElse?.();
|
|
282
|
+
return built === undefined ? undefined : operation.workingRootOf(key, built);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The copy of `uri` the open operation made, without making one. */
|
|
286
|
+
protected existingWorkingRootOf(uri: string): AstNode | undefined {
|
|
287
|
+
return openOperationOf(this)?.existingWorkingRootOf(this.workingKey(uri));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The key of `uri` among an operation's copies: its canonical form, so two
|
|
292
|
+
* spellings of one document share one copy.
|
|
293
|
+
*/
|
|
294
|
+
protected workingKey(uri: string): string {
|
|
295
|
+
return this.sharedServices.workspace.DocumentUriPolicy.canonicalUri(uri);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The built node `node` was copied from during an operation; `node` itself
|
|
300
|
+
* for a built node, for a node the operation created, and outside one.
|
|
301
|
+
*
|
|
302
|
+
* A copy has no `$document`, so a lookup that reads its document, scope or
|
|
303
|
+
* project throws or answers for no document. Handlers pass the built node
|
|
304
|
+
* there and keep editing the copy. A candidate query takes
|
|
305
|
+
* {@link referenceInfoOf} instead, since a created node has no built node.
|
|
306
|
+
*/
|
|
307
|
+
builtNodeOf<T extends AstNode>(node: T): T {
|
|
308
|
+
return openOperationOf(this)?.builtNodeOf(node) ?? node;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* A reference to `target` whose `ref` is `target` itself, a copy node
|
|
313
|
+
* included, so an identity comparison later in the operation finds it.
|
|
314
|
+
* `undefined` when no reference can be formed. Outside an operation the
|
|
315
|
+
* nodes are built ones and this is the `ReferenceBuilder`'s answer as is.
|
|
316
|
+
*
|
|
317
|
+
* With `tier: 'own'` the `$refText` is the target's own name as
|
|
318
|
+
* `toOwnReference` gives it, read off `target` itself, so a name the
|
|
319
|
+
* operation edited is the one written; the builder is the target's
|
|
320
|
+
* language's, which a copy node, having no `$document`, would not otherwise
|
|
321
|
+
* reach. `source` plays no part in that tier.
|
|
156
322
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
323
|
+
* By default it is `getReferenceName` from `source`'s context, with the
|
|
324
|
+
* builder of `source`'s language, since `$refText` encoding follows the
|
|
325
|
+
* grammar writing it. That call reads both nodes' projects, to pick the
|
|
326
|
+
* qualification and to refuse a reference across projects the target's is
|
|
327
|
+
* not visible to, and a copy node has none; so it is asked about the built
|
|
328
|
+
* target, which names a target as built, before any rename in the
|
|
329
|
+
* operation, and about {@link builtContextOf} the source. A target the
|
|
330
|
+
* operation created has no built node, and is asked about without a
|
|
331
|
+
* project.
|
|
332
|
+
*/
|
|
333
|
+
referenceTo<T extends AstNode>(target: T, source?: AstNode, options: { readonly tier?: 'own' } = {}): Reference<T> | undefined {
|
|
334
|
+
const refText =
|
|
335
|
+
options.tier === 'own'
|
|
336
|
+
? this.languageServicesFor(target)?.references.ReferenceBuilder.toOwnReference(target)?.$refText
|
|
337
|
+
: this.languageServicesFor(source ?? target)?.references.ReferenceBuilder.getReferenceName(
|
|
338
|
+
this.builtNodeOf(target),
|
|
339
|
+
source === undefined ? undefined : this.builtContextOf(source)
|
|
340
|
+
);
|
|
341
|
+
return refText === undefined ? undefined : { ref: target, $refText: refText };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* A candidate query for `property` of `node`, to pass to
|
|
346
|
+
* `candidateProviderFor(node)?.find`. It is asked from the built node, and
|
|
347
|
+
* for a node the operation created from a stand-in of its type under the
|
|
348
|
+
* built containers it sits in, one per created level: a scope reads local
|
|
349
|
+
* names off every container up the chain, so anchoring higher drops them.
|
|
350
|
+
* A protocol context cannot express this: an element source resolves only
|
|
351
|
+
* through a `resolveElementByName` override, and through the index only
|
|
352
|
+
* for the nodes it exports. `undefined` for a node in no document and no
|
|
353
|
+
* copy, such as one not attached yet.
|
|
354
|
+
*/
|
|
355
|
+
referenceInfoOf(node: AstNode, property: string): ReferenceInfo | undefined {
|
|
356
|
+
let container = this.builtContextOf(node);
|
|
357
|
+
if (!AstUtils.findRootNode(container).$document) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
const created: AstNode[] = [];
|
|
361
|
+
for (let level: AstNode | undefined = node; level && this.builtNodeOf(level) !== container; level = level.$container) {
|
|
362
|
+
created.unshift(level);
|
|
363
|
+
}
|
|
364
|
+
for (const level of created) {
|
|
365
|
+
container = buildAstNode(this.sharedServices.AstReflection, level.$type, {
|
|
366
|
+
$container: container,
|
|
367
|
+
$containerProperty: level.$containerProperty,
|
|
368
|
+
$containerIndex: level.$containerIndex
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return { reference: { $refText: '', ref: undefined }, container, property };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The built node of `node`, or of its nearest container that has one: a
|
|
376
|
+
* node the operation created has no built node, but its container's
|
|
377
|
+
* document and project are its own.
|
|
378
|
+
*/
|
|
379
|
+
protected builtContextOf(node: AstNode): AstNode {
|
|
380
|
+
const operation = openOperationOf(this);
|
|
381
|
+
for (let current: AstNode | undefined = node; current; current = current.$container) {
|
|
382
|
+
const built: AstNode = this.builtNodeOf(current);
|
|
383
|
+
if (built !== current || operation?.workingUriOf(current) === undefined) {
|
|
384
|
+
return built;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return node;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Take `root`, which a write of this state's own produced, as the source
|
|
392
|
+
* root; during an operation the operation takes it and captures it when it
|
|
393
|
+
* ends.
|
|
394
|
+
*/
|
|
395
|
+
protected captureWrittenRoot(root: TRoot): void {
|
|
396
|
+
const operation = openOperationOf(this);
|
|
397
|
+
if (operation) {
|
|
398
|
+
operation.recordWrittenRoot(root);
|
|
399
|
+
} else {
|
|
400
|
+
this.setSourceRoot(this._sourceUri, root);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Resync after a write whose reconcile dropped the edit: during an
|
|
406
|
+
* operation the operation undoes its side effects, pushes nothing, and
|
|
407
|
+
* captures the document's current root when it ends.
|
|
408
|
+
*/
|
|
409
|
+
protected writeDropped(): void {
|
|
410
|
+
const operation = openOperationOf(this);
|
|
411
|
+
if (operation) {
|
|
412
|
+
operation.recordDropped(this.sharedServices.model.ModelService.getDocument(this._sourceUri)?.parseResult.value);
|
|
413
|
+
} else {
|
|
414
|
+
this.refreshSourceRoot();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Take the document's built root as the source root; during an operation,
|
|
420
|
+
* as the root the operation closes on. A write the document already held,
|
|
421
|
+
* and an undo or redo that wrote nothing while its steps may have rebuilt
|
|
422
|
+
* the document, leave this state on a root that predates the document. The
|
|
423
|
+
* built root can lag the text until the next build, whose refresh captures
|
|
424
|
+
* that, as after {@link writeDropped}.
|
|
162
425
|
*/
|
|
163
|
-
|
|
426
|
+
captureBuiltRoot(): void {
|
|
427
|
+
const operation = openOperationOf(this);
|
|
428
|
+
if (!operation) {
|
|
429
|
+
this.refreshSourceRoot();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const current = this.sharedServices.model.ModelService.getDocument(this._sourceUri)?.parseResult.value;
|
|
433
|
+
if (current !== undefined) {
|
|
434
|
+
operation.recordWrittenRoot(current);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Whether `node` belongs to the root its document is built to now, or to no
|
|
440
|
+
* document; a root a rebuild replaced holds only stale nodes.
|
|
441
|
+
*/
|
|
442
|
+
protected isCurrentBuiltNode(node: AstNode): boolean {
|
|
443
|
+
const root = AstUtils.findRootNode(node);
|
|
444
|
+
const document = root.$document;
|
|
445
|
+
return (
|
|
446
|
+
document === undefined || this.sharedServices.model.ModelService.getDocument(document.uri.toString())?.parseResult.value === root
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The language services of the diagram document: the language
|
|
452
|
+
* {@link sourceUri} routed to at the last {@link setSourceRoot}.
|
|
453
|
+
*
|
|
454
|
+
* `undefined` until a document is loaded, as GLSP builds a session's
|
|
455
|
+
* handlers at `InitializeClientSession`, before `RequestModelAction`
|
|
456
|
+
* supplies the URI: read it when called, not at construction.
|
|
457
|
+
*
|
|
458
|
+
* Reach for it only when the thing you are asking about lives in the
|
|
459
|
+
* diagram document itself. Anything reached through a reference is decided
|
|
460
|
+
* by its own document: use {@link languageServicesFor}.
|
|
461
|
+
*/
|
|
462
|
+
get diagramLanguage(): ServerLanguageServices | undefined {
|
|
463
|
+
return this._sourceLanguage;
|
|
464
|
+
}
|
|
164
465
|
|
|
165
466
|
/**
|
|
166
467
|
* The language services owning `target` — an AST node (routed by its
|
|
167
|
-
* document), a URI, or a URI string — falling back to
|
|
168
|
-
*
|
|
468
|
+
* document), a URI, or a URI string — falling back, for a node, to the
|
|
469
|
+
* language the index read from the diagram's root, and otherwise to
|
|
470
|
+
* {@link diagramLanguage}, when `target` routes nowhere.
|
|
169
471
|
*
|
|
170
472
|
* Use this wherever the thing being named, keyed or completed may live in
|
|
171
473
|
* another document than the diagram. Two registered languages are free to
|
|
@@ -175,8 +477,14 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
175
477
|
* diagnostic. The fallback covers the genuinely unroutable case, a synthetic
|
|
176
478
|
* node not yet attached to a document, where the diagram's own language is
|
|
177
479
|
* the only defensible answer.
|
|
480
|
+
*
|
|
481
|
+
* A node routes through {@link HydraniumGlspIndex.languageServicesFor}, so
|
|
482
|
+
* an override of the index's routing applies here too.
|
|
178
483
|
*/
|
|
179
484
|
languageServicesFor(target: LanguageTarget | undefined): ServerLanguageServices | undefined {
|
|
485
|
+
if (isAstNode(target)) {
|
|
486
|
+
return this.index.languageServicesFor(target) ?? this.diagramLanguage;
|
|
487
|
+
}
|
|
180
488
|
return this.sharedServices.ServiceRegistry.getServicesFor(target) ?? this.diagramLanguage;
|
|
181
489
|
}
|
|
182
490
|
|
|
@@ -206,60 +514,42 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
206
514
|
return this.languageServicesFor(target)?.references.CandidateProvider;
|
|
207
515
|
}
|
|
208
516
|
|
|
209
|
-
/**
|
|
210
|
-
* Warn when the loaded document does not route to the grammar this diagram
|
|
211
|
-
* module declared.
|
|
212
|
-
*
|
|
213
|
-
* The declared language is the one fact the module states that the grammar
|
|
214
|
-
* itself does not, so it is the one that can drift — a diagram type pointed
|
|
215
|
-
* at the wrong `LanguageMetaData`, or a file extension reassigned. Both
|
|
216
|
-
* would otherwise surface much later as references resolving against the
|
|
217
|
-
* wrong scope. Only a warning, not a throw: the document is loaded and
|
|
218
|
-
* usable either way, and a hard failure here would take out a diagram over
|
|
219
|
-
* a mismatch that may be intentional in an adopter serving one diagram type
|
|
220
|
-
* over several grammars.
|
|
221
|
-
*/
|
|
222
|
-
protected checkDeclaredLanguage(uri: string): void {
|
|
223
|
-
const declared = this.diagramLanguage?.LanguageMetaData.languageId;
|
|
224
|
-
if (declared === undefined) {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
const actual = this.sharedServices.ServiceRegistry.getServicesFor(uri)?.LanguageMetaData.languageId;
|
|
228
|
-
if (actual !== undefined && actual !== declared) {
|
|
229
|
-
this.tracer.warn(`Diagram module declares language '${declared}' but this document routes to '${actual}'`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
517
|
/** The observability handle as a plain {@link Logger} (a {@link Tracer} is-a Logger). */
|
|
234
518
|
get logger(): Logger {
|
|
235
519
|
return this.tracer;
|
|
236
520
|
}
|
|
237
521
|
|
|
238
|
-
/** URI-tagged once
|
|
522
|
+
/** URI-tagged once a source root is captured; the class-tagged injected {@link baseTracer} before that. */
|
|
239
523
|
get tracer(): Tracer {
|
|
240
524
|
return this._tracer ?? this.baseTracer;
|
|
241
525
|
}
|
|
242
526
|
|
|
243
527
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* at that point in time — frozen until the next `setSourceRoot`, then
|
|
247
|
-
* re-read from `LangiumDocuments`.
|
|
528
|
+
* Model version of the root the last {@link captureSourceRoot} took,
|
|
529
|
+
* frozen until the next one.
|
|
248
530
|
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* conflict gate against the captured based-on version. See
|
|
531
|
+
* The base an operation's write is gated on, taken when the operation
|
|
532
|
+
* opens; nothing captures during an operation, so it is the version of the
|
|
533
|
+
* root the operation's copies were made from. See
|
|
253
534
|
* `@hydranium/protocol#ConflictError` for the detection contract.
|
|
254
535
|
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
|
|
259
|
-
|
|
536
|
+
* **This, not a number read at write time, is what a write must be gated
|
|
537
|
+
* on.** A version read when the write is issued is the store's current one,
|
|
538
|
+
* so it matches by construction and the gate can never fire.
|
|
539
|
+
*/
|
|
540
|
+
get baseVersion(): ModelVersion {
|
|
541
|
+
return this._baseVersion;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* {@link baseVersion} as a plain number: the store's version of the text the
|
|
546
|
+
* root was parsed from.
|
|
547
|
+
*
|
|
548
|
+
* `UNRECORDED_VERSION` when the document was not built when the snapshot
|
|
549
|
+
* was taken, which no write matches.
|
|
260
550
|
*/
|
|
261
551
|
get version(): number {
|
|
262
|
-
return this.
|
|
552
|
+
return this._baseVersion;
|
|
263
553
|
}
|
|
264
554
|
|
|
265
555
|
// ============================================================
|
|
@@ -267,8 +557,8 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
267
557
|
// ============================================================
|
|
268
558
|
|
|
269
559
|
/**
|
|
270
|
-
* Register `uri` as a document this state also writes,
|
|
271
|
-
* text-document version.
|
|
560
|
+
* Register `uri` as a document this state also writes, snapshotting its
|
|
561
|
+
* current text-document version.
|
|
272
562
|
*
|
|
273
563
|
* A diagram whose layout lives in its own file, or whose canvas creates
|
|
274
564
|
* elements in a referenced semantic document, writes more than the document
|
|
@@ -281,7 +571,7 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
281
571
|
* identified once the operation names the node. What the framework supplies
|
|
282
572
|
* is the part an adopter cannot reconstruct after the fact — the version each
|
|
283
573
|
* document was at BEFORE the command mutated anything (see
|
|
284
|
-
* {@link
|
|
574
|
+
* {@link baseVersionOf}). Idempotent; registering the primary is ignored,
|
|
285
575
|
* since {@link version} already tracks it. Registering a URI not already in
|
|
286
576
|
* the set fires {@link onSecondaryUrisChanged}.
|
|
287
577
|
*/
|
|
@@ -290,7 +580,10 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
290
580
|
return;
|
|
291
581
|
}
|
|
292
582
|
const added = !this._secondaryVersions.has(uri);
|
|
293
|
-
|
|
583
|
+
const key = this.workingKey(uri);
|
|
584
|
+
// During an operation, the root the operation copied or first reached, not a later build's.
|
|
585
|
+
const root = (openOperationOf(this) ? this.capturedRoots.get(key) : undefined) ?? this.readBuiltRoot(uri);
|
|
586
|
+
this.recordSecondary(uri, root);
|
|
294
587
|
if (added) {
|
|
295
588
|
this.secondaryUrisChangedEmitter.fire();
|
|
296
589
|
}
|
|
@@ -310,14 +603,15 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
310
603
|
|
|
311
604
|
/**
|
|
312
605
|
* Fires whenever the secondary write set gains or loses a URI — never when a
|
|
313
|
-
* tracked document's
|
|
314
|
-
* every {@link
|
|
606
|
+
* tracked document's model version is merely refreshed, which happens on
|
|
607
|
+
* every {@link captureSourceRoot} and changes nothing a subscriber cares about.
|
|
315
608
|
*
|
|
316
609
|
* **This exists so no caller has to know when the set can change.** The set is
|
|
317
610
|
* usually discovered rather than known, and the discovery point is an
|
|
318
|
-
* operation handler naming a node — not a
|
|
319
|
-
* re-derived the set after each
|
|
320
|
-
* it silently: the document would simply never be
|
|
611
|
+
* operation handler naming a node — not a read of the source root. A
|
|
612
|
+
* subscriber that instead re-derived the set after each such read would miss
|
|
613
|
+
* exactly that case, and miss it silently: the document would simply never be
|
|
614
|
+
* watched, with nothing logged.
|
|
321
615
|
* Announcing the change here moves the obligation to the one place that can
|
|
322
616
|
* always discharge it.
|
|
323
617
|
*
|
|
@@ -335,39 +629,115 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
335
629
|
return [...this._secondaryVersions.keys()];
|
|
336
630
|
}
|
|
337
631
|
|
|
632
|
+
/** Active statuses, least recently set first. See {@link setStatus}. */
|
|
633
|
+
protected readonly statuses = new Map<DiagramStatus, DiagramStatusEntry>();
|
|
634
|
+
|
|
635
|
+
/** Backing emitter for {@link onStatusChanged}. */
|
|
636
|
+
protected readonly statusChangedEmitter = new Emitter<void>();
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Set `status`'s entry, or withdraw it with `undefined`, and recompute
|
|
640
|
+
* {@link currentStatus} and {@link editMode} from every active entry.
|
|
641
|
+
*
|
|
642
|
+
* `editMode` is derived here and nowhere else: a writer that assigns it
|
|
643
|
+
* directly is overwritten by the next status change, and its write is not
|
|
644
|
+
* announced on {@link onStatusChanged}.
|
|
645
|
+
*/
|
|
646
|
+
setStatus(status: DiagramStatus, entry: DiagramStatusEntry | undefined): void {
|
|
647
|
+
const previousEntry = this.statuses.get(status);
|
|
648
|
+
if (previousEntry === undefined && entry === undefined) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const previousStatus = this.currentStatus;
|
|
652
|
+
const previousEditMode = this.editMode;
|
|
653
|
+
// Re-inserted so iteration order is recency, which breaks a severity tie.
|
|
654
|
+
this.statuses.delete(status);
|
|
655
|
+
if (entry !== undefined) {
|
|
656
|
+
this.statuses.set(status, entry);
|
|
657
|
+
}
|
|
658
|
+
this.editMode = [...this.statuses.values()].some(active => active.readonly) ? EditMode.READONLY : EditMode.EDITABLE;
|
|
659
|
+
this.logger.debug(`Status ${status}: ${describeStatusEntry(previousEntry)} → ${describeStatusEntry(entry)}`);
|
|
660
|
+
if (this.currentStatus !== previousStatus || this.editMode !== previousEditMode) {
|
|
661
|
+
this.statusChangedEmitter.fire();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** The active entry whose message the client shows: highest severity, then most recently set. */
|
|
666
|
+
get currentStatus(): DiagramStatusEntry | undefined {
|
|
667
|
+
let winner: DiagramStatusEntry | undefined;
|
|
668
|
+
for (const entry of this.statuses.values()) {
|
|
669
|
+
if (entry.message !== undefined && (!winner || severityRank(entry) >= severityRank(winner))) {
|
|
670
|
+
winner = entry;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return winner;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** The active statuses that block editing; empty while the diagram is editable. */
|
|
677
|
+
get readonlyStatuses(): DiagramStatus[] {
|
|
678
|
+
return [...this.statuses].filter(([, entry]) => entry.readonly).map(([status]) => status);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** The status that supplies {@link currentStatus}, for a log line naming it. */
|
|
682
|
+
get currentStatusSource(): DiagramStatus | undefined {
|
|
683
|
+
const current = this.currentStatus;
|
|
684
|
+
return [...this.statuses].find(([, entry]) => entry === current)?.[0];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Fires when {@link currentStatus} or {@link editMode} changes. */
|
|
688
|
+
get onStatusChanged(): Event<void> {
|
|
689
|
+
return this.statusChangedEmitter.event;
|
|
690
|
+
}
|
|
691
|
+
|
|
338
692
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
* {@link
|
|
693
|
+
* The base version of a write of `uri`: {@link baseVersion} for the primary,
|
|
694
|
+
* the version taken at {@link trackSecondaryDocument} (refreshed by
|
|
695
|
+
* {@link captureSourceRoot}) for a secondary, `undefined` for anything untracked.
|
|
342
696
|
*
|
|
343
|
-
* `undefined` rather than
|
|
697
|
+
* `undefined` rather than v0 for an untracked URI deliberately: `0` is a real
|
|
344
698
|
* version meaning "present but never edited", so collapsing the two would let
|
|
345
|
-
* a caller gate a write against a document
|
|
699
|
+
* a caller gate a write against a document this state never read.
|
|
346
700
|
*/
|
|
347
|
-
|
|
701
|
+
baseVersionOf(uri: string): ModelVersion | undefined {
|
|
348
702
|
if (uri === this._sourceUri) {
|
|
349
|
-
return this.
|
|
703
|
+
return this._baseVersion;
|
|
350
704
|
}
|
|
351
705
|
return this._secondaryVersions.get(uri);
|
|
352
706
|
}
|
|
353
707
|
|
|
708
|
+
/** Re-take every registered secondary's built root and the model version of that root. */
|
|
709
|
+
protected refreshSecondaryVersions(): void {
|
|
710
|
+
this.capturedRoots.clear();
|
|
711
|
+
for (const uri of [...this._secondaryVersions.keys()]) {
|
|
712
|
+
this.recordSecondary(uri, this.readBuiltRoot(uri));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
354
716
|
/**
|
|
355
|
-
*
|
|
356
|
-
* canonicalizing gateway
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
717
|
+
* The built root of `uri`, looked up through the model service's
|
|
718
|
+
* canonicalizing gateway so a symlinked URI does not strand without one;
|
|
719
|
+
* `undefined` for a URI with no parsed document, whose version is then
|
|
720
|
+
* `UNRECORDED_VERSION`, which no write matches. The store counts a file it
|
|
721
|
+
* has not built at `0`, and so does the placeholder the builder registers
|
|
722
|
+
* before its parse, and a write based on `0` overwrites text this state
|
|
723
|
+
* never read.
|
|
361
724
|
*/
|
|
362
|
-
protected
|
|
363
|
-
|
|
725
|
+
protected readBuiltRoot(uri: string): AstNode | undefined {
|
|
726
|
+
const root = this.sharedServices.model.ModelService.getDocument(uri)?.parseResult.value;
|
|
727
|
+
return root && !this.sharedServices.workspace.ModelLedger.isPlaceholder(root) ? root : undefined;
|
|
364
728
|
}
|
|
365
729
|
|
|
366
|
-
/**
|
|
367
|
-
protected
|
|
368
|
-
|
|
369
|
-
this.
|
|
730
|
+
/** Take `root` as the secondary `uri` was read at, and its version, `UNRECORDED_VERSION` without one. */
|
|
731
|
+
protected recordSecondary(uri: string, root: AstNode | undefined): void {
|
|
732
|
+
if (root !== undefined) {
|
|
733
|
+
this.capturedRoots.set(this.workingKey(uri), root);
|
|
370
734
|
}
|
|
735
|
+
this._secondaryVersions.set(uri, root ? this.sharedServices.workspace.ModelLedger.versionOf(root) : UNRECORDED_VERSION);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** The root the secondary `uri` was read at; see {@link capturedRoots}. */
|
|
739
|
+
protected capturedRootOf(uri: string): AstNode | undefined {
|
|
740
|
+
return this.capturedRoots.get(this.workingKey(uri));
|
|
371
741
|
}
|
|
372
742
|
|
|
373
743
|
/**
|
|
@@ -378,7 +748,8 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
378
748
|
* handlers) must see the root the freshly built document carries.
|
|
379
749
|
*/
|
|
380
750
|
protected refreshSourceRoot(): void {
|
|
381
|
-
|
|
751
|
+
// The operation captures when it ends.
|
|
752
|
+
if (!this._sourceUri || openOperationOf(this)) {
|
|
382
753
|
return;
|
|
383
754
|
}
|
|
384
755
|
const document = this.sharedServices.model.ModelService.getDocument(this._sourceUri);
|
|
@@ -392,10 +763,45 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
392
763
|
}
|
|
393
764
|
}
|
|
394
765
|
|
|
766
|
+
/**
|
|
767
|
+
* The root that `uri`'s text parses to (the text store's, or the built
|
|
768
|
+
* document's when the store holds none) and the store's version of that
|
|
769
|
+
* text, read once the document has validated; `undefined` when it cannot be
|
|
770
|
+
* read. The version is read in the tick the text is, so a write based on it
|
|
771
|
+
* conflicts with any edit made after.
|
|
772
|
+
*
|
|
773
|
+
* **Parsed afresh, never the built root.** The built root can lag the
|
|
774
|
+
* store, and a root behind the text it is versioned with makes the replay
|
|
775
|
+
* drop the edits it lacks. The root is parsed, not linked: an encoder hook
|
|
776
|
+
* reading a reference's `ref` sees `undefined` on it.
|
|
777
|
+
*/
|
|
778
|
+
protected async readCurrentRoot(uri: string): Promise<{ root: AstNode; version: ModelVersion } | undefined> {
|
|
779
|
+
const modelService = this.sharedServices.model.ModelService;
|
|
780
|
+
// Unreadable here is the conflict the caller then raises, which is logged where it ends; the reason is diagnosis.
|
|
781
|
+
const validated = await modelService.validated(uri).catch((error: unknown) => {
|
|
782
|
+
this.logger.debug(`The current text of ${uri} cannot be read: ${describeError(error)}`);
|
|
783
|
+
return undefined;
|
|
784
|
+
});
|
|
785
|
+
if (!validated) {
|
|
786
|
+
return undefined;
|
|
787
|
+
}
|
|
788
|
+
const store = this.sharedServices.workspace.TextDocuments;
|
|
789
|
+
const text = store.get(uri)?.getText() ?? modelService.getDocument(uri)?.textDocument.getText();
|
|
790
|
+
if (text === undefined) {
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
const version = asModelVersion(store.version(uri));
|
|
794
|
+
return { root: this.sharedServices.workspace.LangiumDocumentFactory.fromString(text, URI.parse(uri)).parseResult.value, version };
|
|
795
|
+
}
|
|
796
|
+
|
|
395
797
|
/**
|
|
396
798
|
* Wait until the captured document reaches `state`, then run
|
|
397
799
|
* {@link onReadyRefreshed} so consumers see a fresh AST. Wraps the
|
|
398
800
|
* wait in {@link Tracer.time} for observability.
|
|
801
|
+
*
|
|
802
|
+
* A source not in the document registry is built first, through
|
|
803
|
+
* `ModelService.rebuild`. For a source with neither a file nor text, that
|
|
804
|
+
* build leaves no document, and this rejects rather than timing out.
|
|
399
805
|
*/
|
|
400
806
|
async ready(state: DocumentState): Promise<void> {
|
|
401
807
|
await this.tracer.time(`Wait for state '${DocumentState[state]}'`, () => this.readyWithTimeout(state), 'debug');
|
|
@@ -403,40 +809,43 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
403
809
|
}
|
|
404
810
|
|
|
405
811
|
/**
|
|
406
|
-
* Race `ModelService.
|
|
407
|
-
* {@link readyTimeoutMs}. If the timeout fires but the
|
|
408
|
-
* already reached `state
|
|
409
|
-
*
|
|
410
|
-
* {@link ModelReadyTimeoutError} via {@link buildReadyTimeoutError}.
|
|
812
|
+
* Race `ModelService.ensureDocumentState`, which builds a root behind its
|
|
813
|
+
* text, against {@link readyTimeoutMs}. If the timeout fires but the
|
|
814
|
+
* document has already reached `state`, log a warning naming the cause, a
|
|
815
|
+
* root behind its text or a missed phase event, and resolve; otherwise
|
|
816
|
+
* raise {@link ModelReadyTimeoutError} via {@link buildReadyTimeoutError}.
|
|
411
817
|
*/
|
|
412
818
|
protected async readyWithTimeout(state: DocumentState): Promise<void> {
|
|
413
819
|
const uri = this._sourceUri;
|
|
414
820
|
const stopwatch = this.sharedServices.Clock.stopwatch();
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
timer?.dispose();
|
|
821
|
+
const reached = this.sharedServices.model.ModelService.ensureDocumentState(uri, state);
|
|
822
|
+
if ((await this.sharedServices.Clock.raceTimer(reached, this.readyTimeoutMs)) !== TIMED_OUT) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
// The race has settled, so a later failure of the wait, such as a given-up sync build, reaches no one else.
|
|
826
|
+
void reached.catch((error: unknown) =>
|
|
827
|
+
this.logger.error(`The wait for '${DocumentState[state]}' failed after it timed out: ${describeErrorWithStack(error)}`)
|
|
828
|
+
);
|
|
829
|
+
const elapsed = Math.round(stopwatch.elapsedMs);
|
|
830
|
+
// Through the model service's canonicalizing gateway, so a symlinked
|
|
831
|
+
// `_sourceUri` is not reported as a hard timeout.
|
|
832
|
+
const doc = this.sharedServices.model.ModelService.getDocument(uri);
|
|
833
|
+
if (!doc || doc.state < state) {
|
|
834
|
+
throw this.buildReadyTimeoutError(state, elapsed);
|
|
835
|
+
}
|
|
836
|
+
const built = this.sharedServices.workspace.ModelLedger.versionOf(doc.parseResult.value);
|
|
837
|
+
const text = this.sharedServices.workspace.TextDocuments.version(uri);
|
|
838
|
+
if (built !== UNRECORDED_VERSION && built < text) {
|
|
839
|
+
this.logger.warn(
|
|
840
|
+
`No build caught the root up after ${elapsed}ms: the document is at state '${DocumentState[doc.state]}', ` +
|
|
841
|
+
`but its root was parsed from v${built} and the text is at v${text}.`
|
|
842
|
+
);
|
|
843
|
+
return;
|
|
439
844
|
}
|
|
845
|
+
this.logger.warn(
|
|
846
|
+
`Missed '${DocumentState[state]}' notification after ${elapsed}ms; document is already at state ` +
|
|
847
|
+
`'${DocumentState[doc.state]}'. Likely a Langium build-phase event race.`
|
|
848
|
+
);
|
|
440
849
|
}
|
|
441
850
|
|
|
442
851
|
/**
|
|
@@ -466,11 +875,13 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
466
875
|
}
|
|
467
876
|
|
|
468
877
|
/**
|
|
469
|
-
* Persist a new source-model representation back to the document store
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
878
|
+
* Persist a new source-model representation back to the document store after
|
|
879
|
+
* an interactive edit. Adopters wire this against their language-services
|
|
880
|
+
* facade. An operation calls it once, with the model projected from its
|
|
881
|
+
* copies; an undo or redo calls it with the transition resolved onto the
|
|
882
|
+
* current model. An implementation takes the root it wrote through
|
|
883
|
+
* {@link captureWrittenRoot}, which during an operation leaves the capture
|
|
884
|
+
* to the operation.
|
|
474
885
|
*
|
|
475
886
|
* Adopters whose source model is a structured transfer projection
|
|
476
887
|
* round-tripped through `ModelService` should extend
|
|
@@ -491,13 +902,52 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
491
902
|
* `JsonModelState` shape meet the framework state via structural
|
|
492
903
|
* intersection at the recording-command call site.
|
|
493
904
|
*
|
|
494
|
-
* `
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
|
|
502
|
-
|
|
905
|
+
* `baseVersion` is what the write was authored against: an operation's
|
|
906
|
+
* write, and an undo's or redo's, passes the {@link baseVersion} it read
|
|
907
|
+
* the model at. Adopters that write through the diagram's session forward
|
|
908
|
+
* it as the args' `baseVersion` field; adopters whose write path doesn't go
|
|
909
|
+
* through the session ignore it.
|
|
910
|
+
*
|
|
911
|
+
* **Optional, and every implementation must default it to {@link baseVersion}
|
|
912
|
+
* rather than to `'any'`.** Optional is forced: GLSP's `JsonModelState`
|
|
913
|
+
* declares a one-parameter `updateSourceModel` and calls it with one
|
|
914
|
+
* argument, so a second required parameter makes the state unassignable to
|
|
915
|
+
* the slot it has to fill. Defaulting to the state's own model version is
|
|
916
|
+
* what keeps that from costing anything — an operation handler calling
|
|
917
|
+
* `updateSourceModel(model)` gets the gate, and skipping it has to be typed
|
|
918
|
+
* out as `'any'`.
|
|
919
|
+
*/
|
|
920
|
+
abstract updateSourceModel(model: TSourceModel, baseVersion?: BaseVersion): MaybePromise<void>;
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* `transition` as the documents would hold both ends after a write. A
|
|
924
|
+
* serializer may normalize what it writes, provided normalizing again
|
|
925
|
+
* changes nothing; an end left as recorded makes an undo or redo conflict
|
|
926
|
+
* with the write it follows. Default: unchanged.
|
|
927
|
+
*/
|
|
928
|
+
async normalizeTransition(transition: OperationTransition<TSourceModel>): Promise<OperationTransition<TSourceModel>> {
|
|
929
|
+
return transition;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** The root `model` parses back to once serialized for `uri`, as a write would leave it; nothing is written. */
|
|
933
|
+
protected async roundTrip(uri: string, model: TransferElement): Promise<AstNode> {
|
|
934
|
+
const text = await this.sharedServices.model.ModelService.modelToText(uri, model);
|
|
935
|
+
return this.sharedServices.workspace.LangiumDocumentFactory.fromString(text, URI.parse(uri)).parseResult.value;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const SEVERITY_RANK: Readonly<Record<NonNullable<DiagramStatusEntry['severity']>, number>> = { INFO: 0, WARNING: 1, ERROR: 2, FATAL: 3 };
|
|
940
|
+
|
|
941
|
+
/** A message without a severity ranks as `INFO`. */
|
|
942
|
+
function severityRank(entry: DiagramStatusEntry): number {
|
|
943
|
+
return SEVERITY_RANK[entry.severity ?? 'INFO'];
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function describeStatusEntry(entry: DiagramStatusEntry | undefined): string {
|
|
947
|
+
if (entry === undefined) {
|
|
948
|
+
return 'inactive';
|
|
949
|
+
}
|
|
950
|
+
const flags = [entry.message === undefined ? undefined : (entry.severity ?? 'INFO'), entry.readonly ? 'readonly' : undefined];
|
|
951
|
+
const message = entry.message === undefined ? '' : ` "${entry.message}"`;
|
|
952
|
+
return `${flags.filter(flag => flag !== undefined).join(' ') || 'active'}${message}`;
|
|
503
953
|
}
|