@hydranium/glsp-server 1.0.0-next.31 → 1.0.0-next.315
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
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { MarkersReason, SetMarkersAction } from '@eclipse-glsp/protocol';
|
|
11
10
|
import {
|
|
12
11
|
type Action,
|
|
13
12
|
ActionDispatcher,
|
|
@@ -17,41 +16,64 @@ import {
|
|
|
17
16
|
CommandStack,
|
|
18
17
|
type DefaultCommandStack,
|
|
19
18
|
type Disposable,
|
|
20
|
-
EditMode,
|
|
21
19
|
GLSPServerError,
|
|
22
20
|
Logger as GlspLogger,
|
|
21
|
+
type Marker,
|
|
22
|
+
MarkersReason,
|
|
23
23
|
type MaybePromise,
|
|
24
|
+
MessageAction,
|
|
24
25
|
ModelState,
|
|
25
26
|
ModelSubmissionHandler,
|
|
26
27
|
ModelValidator,
|
|
27
28
|
type RequestModelAction,
|
|
28
29
|
SOURCE_URI_ARG,
|
|
29
30
|
type SaveModelAction,
|
|
30
|
-
|
|
31
|
+
SetDirtyStateAction,
|
|
32
|
+
SetMarkersAction,
|
|
31
33
|
type SourceModelStorage,
|
|
32
34
|
TEMPORARY_CLIENT_ID
|
|
33
35
|
} from '@eclipse-glsp/server';
|
|
34
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
Debouncer,
|
|
38
|
+
defineMessage,
|
|
39
|
+
DisposableCollection,
|
|
40
|
+
isDuplicateClientIdError,
|
|
41
|
+
isSessionClosedError,
|
|
42
|
+
RESUME_TOKEN_ARG,
|
|
43
|
+
SessionClosedError,
|
|
44
|
+
describeError,
|
|
45
|
+
describeErrorWithStack
|
|
46
|
+
} from '@hydranium/protocol';
|
|
35
47
|
import { inject, injectable, optional, postConstruct } from 'inversify';
|
|
36
|
-
import { type AstNode } from '@hydranium/langium';
|
|
37
|
-
import { URI } from '@hydranium/langium';
|
|
38
|
-
import {
|
|
48
|
+
import { type AstNode, type ParseResult } from '@hydranium/langium';
|
|
49
|
+
import { URI, UriUtils } from '@hydranium/langium';
|
|
50
|
+
import {
|
|
51
|
+
type AstDocument,
|
|
52
|
+
type AstDocumentSavedEvent,
|
|
53
|
+
type AstDocumentUpdatedEvent,
|
|
54
|
+
type ClientSession as ModelClientSession,
|
|
55
|
+
type ModelDirtyChangedEvent,
|
|
56
|
+
type ServerSharedServices,
|
|
57
|
+
type SessionEndCause
|
|
58
|
+
} from '@hydranium/core';
|
|
39
59
|
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
40
60
|
import { type AbstractHydraniumGlspState } from '../state/abstract-hydranium-glsp-state.js';
|
|
61
|
+
import { DiagramStatus, type DiagramStatusEntry } from '../state/diagram-status.js';
|
|
41
62
|
import { type HydraniumGlspSubmissionHandler } from '../submission/hydranium-glsp-submission-handler.js';
|
|
42
63
|
import { HydraniumTypes } from '../state/hydranium-shared-core-services.js';
|
|
43
|
-
import {
|
|
64
|
+
import { DEFAULT_SAVE_DELIVERY_POLICY, SaveDeliveryPolicy } from './save-delivery-policy.js';
|
|
44
65
|
|
|
45
66
|
/**
|
|
46
|
-
* A save action arrived with nowhere to write to.
|
|
67
|
+
* A save action arrived with nowhere to write to. A save action carries no
|
|
68
|
+
* request id, so it falls through to the error handler and `message` becomes
|
|
69
|
+
* the toast text.
|
|
47
70
|
*
|
|
48
|
-
*
|
|
49
|
-
* action
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* `cause?.toString?.()`, so a code there would sit inside human text.
|
|
71
|
+
* Rendered at the raise site rather than by a carrier method, because GLSP's
|
|
72
|
+
* action protocol has no slot for an identity anywhere — every member of its
|
|
73
|
+
* message, status and reject actions is prose or an enum, and
|
|
74
|
+
* `MessageAction.details` is not a substitute since it is prose populated from
|
|
75
|
+
* `cause?.toString?.()`, and a Theia host shows it to a user on demand. So the
|
|
76
|
+
* throw is the last place that still knows which message this is.
|
|
55
77
|
*/
|
|
56
78
|
export const SAVE_TARGET_UNKNOWN = defineMessage(
|
|
57
79
|
'hydranium/glsp-server/save-target-unknown',
|
|
@@ -60,16 +82,54 @@ export const SAVE_TARGET_UNKNOWN = defineMessage(
|
|
|
60
82
|
|
|
61
83
|
/**
|
|
62
84
|
* A model request arrived without the source URI it is required to carry.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* protocol has no slot for an identity, so the English is what a client
|
|
66
|
-
* receives.
|
|
85
|
+
* Rendered at the raise site, like its save-path sibling and for the same
|
|
86
|
+
* reason.
|
|
67
87
|
*/
|
|
68
88
|
export const SOURCE_URI_MISSING = defineMessage(
|
|
69
89
|
'hydranium/glsp-server/source-uri-missing',
|
|
70
90
|
'Could not open this model: the request did not say which document to load'
|
|
71
91
|
);
|
|
72
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Why the canvas has stopped accepting edits.
|
|
95
|
+
*
|
|
96
|
+
* **A READONLY canvas is otherwise indistinguishable from a broken one.** The
|
|
97
|
+
* client's answer to a read-only edit mode is to withdraw the tool palette,
|
|
98
|
+
* so the surface a reader was working in silently loses the only control it had,
|
|
99
|
+
* with the cause — a syntax error in a document that may not even be open —
|
|
100
|
+
* nowhere on screen. The mode flip is the mechanism; this is the only part of it
|
|
101
|
+
* a user can see.
|
|
102
|
+
*
|
|
103
|
+
* Rendered at the raise site, like its two siblings above and for the same
|
|
104
|
+
* reason: every member of GLSP's status action is prose or an enum.
|
|
105
|
+
*
|
|
106
|
+
* It names the document and the recovery rather than the fault, because the
|
|
107
|
+
* fault already has a surface — the squiggle and the problems list, both of which
|
|
108
|
+
* say WHICH character — and repeating it here would put a second, less precise
|
|
109
|
+
* account of the same error on screen. What no other surface says is that the
|
|
110
|
+
* diagram is waiting on it, and the document it waits on need not be the one
|
|
111
|
+
* the diagram was opened on.
|
|
112
|
+
*/
|
|
113
|
+
export const DIAGRAM_READONLY_PARSE_ERROR = defineMessage(
|
|
114
|
+
'hydranium/glsp-server/diagram-readonly-parse-error',
|
|
115
|
+
'Read-only: {document} has a syntax error. Fix it to edit the diagram again.'
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The diagram's GLSP client id is still live as another participant's client
|
|
120
|
+
* id once the load has waited for it to free, so the diagram does not load.
|
|
121
|
+
*
|
|
122
|
+
* Rendered at the raise site, as {@link SAVE_TARGET_UNKNOWN} is and for the
|
|
123
|
+
* same reason, and dispatched to the client as a message of its own: a model
|
|
124
|
+
* request fails as a rejection whose detail is the English cause, which no user
|
|
125
|
+
* sees. It names the recovery: a host that numbers its diagram clients gives a
|
|
126
|
+
* reopened diagram a fresh id.
|
|
127
|
+
*/
|
|
128
|
+
export const DIAGRAM_SESSION_REFUSED = defineMessage(
|
|
129
|
+
'hydranium/glsp-server/diagram-session-refused',
|
|
130
|
+
'Could not open this diagram: its identifier is still in use by another editor. Close the diagram and open it again.'
|
|
131
|
+
);
|
|
132
|
+
|
|
73
133
|
/** Window (ms) over which back-to-back external rebuilds collapse into one resubmit. */
|
|
74
134
|
const EXTERNAL_SUBMIT_DEBOUNCE_MS = 250;
|
|
75
135
|
|
|
@@ -98,6 +158,16 @@ function isStructuralDiagnostic(diagnostic: unknown): boolean {
|
|
|
98
158
|
return candidate.severity === DiagnosticSeverity.Error && (code === 'lexing-error' || code === 'parsing-error');
|
|
99
159
|
}
|
|
100
160
|
|
|
161
|
+
/** Whether `parseResult` holds what Langium's validator reports as an error-severity `lexing-error` or `parsing-error`. */
|
|
162
|
+
function hasStructuralErrors(parseResult: ParseResult): boolean {
|
|
163
|
+
const lexing = parseResult.lexerReport?.diagnostics ?? [];
|
|
164
|
+
return (
|
|
165
|
+
parseResult.parserErrors.length > 0 ||
|
|
166
|
+
parseResult.lexerErrors.length > 0 ||
|
|
167
|
+
lexing.some(diagnostic => (diagnostic.severity ?? 'error') === 'error')
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
101
171
|
/**
|
|
102
172
|
* GLSP source-model storage base shared by all hydranium adopters.
|
|
103
173
|
* Provides the DI signature, the {@link ClientSessionListener} +
|
|
@@ -108,27 +178,37 @@ function isStructuralDiagnostic(diagnostic: unknown): boolean {
|
|
|
108
178
|
* invariant.
|
|
109
179
|
*
|
|
110
180
|
* **The settled-root invariant.** Every {@link AbstractHydraniumGlspState.setSourceRoot}
|
|
111
|
-
* the
|
|
181
|
+
* the storage performs captures a root that has reached
|
|
112
182
|
* `IntegrityService.SettledState` (post-`Linked`, references indexed, on-build
|
|
113
|
-
* integrity rules applied). The
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* document
|
|
183
|
+
* integrity rules applied). The end of an operation captures the root its
|
|
184
|
+
* write produced, settled or not, and the operation's own submit waits for the
|
|
185
|
+
* settled one through `ready()`. The GModel factory therefore always walks a
|
|
186
|
+
* fully linked + reprojected AST. The resubmit path re-`settled()`s rather
|
|
187
|
+
* than keeping the `onModelUpdated` event's document: the event's document
|
|
188
|
+
* holds only while its listener runs, and the resubmit runs after a debounce,
|
|
189
|
+
* by which time another build can have reset it.
|
|
118
190
|
*
|
|
119
191
|
* Adopters with richer needs override the seams ({@link isStructurallyBroken},
|
|
120
|
-
* {@link
|
|
121
|
-
* whole flow, and select a {@link
|
|
122
|
-
* tune how {@link saveSourceModel}
|
|
192
|
+
* {@link parseErrorStatus}, {@link onSourceModelSettled}) rather than the
|
|
193
|
+
* whole flow, and select a {@link SaveDeliveryPolicy} via the bound option to
|
|
194
|
+
* tune how {@link saveSourceModel} delivers its result.
|
|
195
|
+
*
|
|
196
|
+
* **One client session per GLSP client session.** The load registers the GLSP
|
|
197
|
+
* client id as a client session ({@link registerModelSession}), with the
|
|
198
|
+
* request's {@link RESUME_TOKEN_ARG}, and hands it to the state as
|
|
199
|
+
* `modelSession`. The primary is opened through it, and
|
|
200
|
+
* every document that joins the write set as it joins
|
|
201
|
+
* ({@link openSecondary}); a document that leaves the write set stays open
|
|
202
|
+
* until the next save, so leaving never reverts the diagram's unsaved edits to
|
|
203
|
+
* it. Disposing the storage ends the session, which closes everything it has
|
|
204
|
+
* open.
|
|
123
205
|
*
|
|
124
|
-
* **Default `saveSourceModel` flow.**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* `overwrite`) decides the based-on-version guard, await-vs-fire-and-forget, and
|
|
131
|
-
* failure handling.
|
|
206
|
+
* **Default `saveSourceModel` flow.** Flushes the stored text of every
|
|
207
|
+
* document the diagram's session has open through the session's `persist`, with
|
|
208
|
+
* no serializer in the path — the update path already put the settled text in
|
|
209
|
+
* the store. The bound {@link SaveDeliveryPolicy} (default
|
|
210
|
+
* {@link DEFAULT_SAVE_DELIVERY_POLICY}, `await`) decides
|
|
211
|
+
* await-vs-fire-and-forget and failure handling.
|
|
132
212
|
*
|
|
133
213
|
* **tempId filter.** GLSP's `DefaultGlobalActionProvider` spins up a
|
|
134
214
|
* throwaway per-diagram-type container with upstream's {@link TEMPORARY_CLIENT_ID}
|
|
@@ -139,9 +219,12 @@ function isStructuralDiagnostic(diagnostic: unknown): boolean {
|
|
|
139
219
|
* **Lifecycle ordering.** `init` runs after DI resolution and parks the
|
|
140
220
|
* secondary-write-set watch, which outlives any single load.
|
|
141
221
|
* {@link sessionDisposed} calls {@link dispose}, which drains
|
|
142
|
-
* {@link toDispose} idempotently
|
|
143
|
-
*
|
|
144
|
-
*
|
|
222
|
+
* {@link toDispose} idempotently and then ends the client session. The
|
|
223
|
+
* framework's GLSP server disposes the storage as lost first when it shuts
|
|
224
|
+
* down, which is how the client's connection ending reaches it, so the
|
|
225
|
+
* diagram's unsaved text waits out the release grace. Every
|
|
226
|
+
* transient subscription created in {@link doLoadSourceModel} is parked on
|
|
227
|
+
* {@link toDispose} so the drain catches them on client-detach.
|
|
145
228
|
*/
|
|
146
229
|
@injectable()
|
|
147
230
|
export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
@@ -165,14 +248,13 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
165
248
|
@inject(ModelValidator) @optional() protected readonly modelValidator?: ModelValidator;
|
|
166
249
|
|
|
167
250
|
/**
|
|
168
|
-
* Selected {@link
|
|
169
|
-
* {@link
|
|
170
|
-
* left unbound it resolves to {@link
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* `@optional()` member.
|
|
251
|
+
* Selected {@link SaveDeliveryPolicy} for {@link saveSourceModel}. Bind the
|
|
252
|
+
* {@link SaveDeliveryPolicy} token in a `DiagramModule` to choose a policy;
|
|
253
|
+
* left unbound it resolves to {@link DEFAULT_SAVE_DELIVERY_POLICY}. Read via
|
|
254
|
+
* {@link saveDeliveryPolicy} so the fallback is applied even when inversify
|
|
255
|
+
* injects `undefined` for an unbound `@optional()` member.
|
|
174
256
|
*/
|
|
175
|
-
@inject(
|
|
257
|
+
@inject(SaveDeliveryPolicy) @optional() protected readonly boundSaveDeliveryPolicy?: SaveDeliveryPolicy;
|
|
176
258
|
|
|
177
259
|
/** Disposables created during {@link doLoadSourceModel}; drained on session disposal. */
|
|
178
260
|
protected toDispose = new DisposableCollection();
|
|
@@ -188,13 +270,20 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
188
270
|
protected readonly secondarySubscriptions = new Map<string, Disposable>();
|
|
189
271
|
|
|
190
272
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* only place that sees operation submits too.
|
|
273
|
+
* Documents that left the write set since the last save. They stay open in
|
|
274
|
+
* the diagram's session until that save has persisted them, and are closed
|
|
275
|
+
* after it: closing a document as it leaves would revert the diagram's
|
|
276
|
+
* unsaved edits to it where the diagram was its last client.
|
|
196
277
|
*/
|
|
197
|
-
protected
|
|
278
|
+
protected readonly departedSecondaries = new Set<string>();
|
|
279
|
+
|
|
280
|
+
/** How long a load without a resume token waits for a held client id to free; see {@link loadModelSession}. */
|
|
281
|
+
protected readonly sessionWaitMs: number = 2_000;
|
|
282
|
+
|
|
283
|
+
/** Set by {@link dispose}; a disposed storage registers no session again. */
|
|
284
|
+
protected disposed = false;
|
|
285
|
+
/** Saves of the diagram's own that GLSP's save handler awaits; see {@link handleDirtyChanged}. */
|
|
286
|
+
protected ownSavesPending = 0;
|
|
198
287
|
|
|
199
288
|
/**
|
|
200
289
|
* Trailing-edge debounce for the external resubmit. Rescheduled on each
|
|
@@ -205,9 +294,6 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
205
294
|
*/
|
|
206
295
|
protected resubmitDebouncer!: Debouncer;
|
|
207
296
|
|
|
208
|
-
/** Latest event document awaiting the debounced resubmit; read by {@link flushResubmit} (last write wins). */
|
|
209
|
-
protected pendingResubmitDocument?: AstDocument<AstNode, unknown>;
|
|
210
|
-
|
|
211
297
|
@postConstruct()
|
|
212
298
|
protected init(): void {
|
|
213
299
|
this.resubmitDebouncer = new Debouncer(this.sharedServices.Clock, () => this.flushResubmit(), {
|
|
@@ -224,6 +310,110 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
224
310
|
this.toDispose.push(this.state.onSecondaryUrisChanged(() => this.reconcileSecondarySubscriptions()));
|
|
225
311
|
}
|
|
226
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Register the diagram's client session under the GLSP client id, or
|
|
315
|
+
* `undefined` when another participant holds that id, which is logged, and
|
|
316
|
+
* the diagram then refuses to load ({@link requireModelSession}). Taking
|
|
317
|
+
* the id over would end that participant's session under it, and working
|
|
318
|
+
* without a session would share its opens and echoes, so its close would
|
|
319
|
+
* take the diagram's documents with it.
|
|
320
|
+
*
|
|
321
|
+
* The exception is a `resumeToken` the holder registered with, which takes
|
|
322
|
+
* its session over: a diagram that reconnected or reloaded, whose old
|
|
323
|
+
* connection the server has not seen close yet.
|
|
324
|
+
*
|
|
325
|
+
* A client id the framework reserves throws `ReservedClientIdError`:
|
|
326
|
+
* whoever chose the GLSP client id has a bug, and no later registration can
|
|
327
|
+
* succeed.
|
|
328
|
+
*/
|
|
329
|
+
protected registerModelSession(resumeToken?: string): ModelClientSession<AstNode> | undefined {
|
|
330
|
+
try {
|
|
331
|
+
return this.sharedServices.model.ModelService.createSession('diagram', this.state.clientId, { resumeToken });
|
|
332
|
+
} catch (error: unknown) {
|
|
333
|
+
if (!isDuplicateClientIdError(error)) {
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
// Routine while a reconnecting diagram's old session has not closed yet.
|
|
337
|
+
this.logger.debug(`Client id ${this.state.clientId} is held by another participant: this diagram has no session yet`);
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The diagram's client session. Throws `SessionClosedError` once the
|
|
344
|
+
* storage is disposed, and the refusal before a load has registered one.
|
|
345
|
+
* Registering here instead would start the session without the load's
|
|
346
|
+
* resume token, which a later resume then fails to match.
|
|
347
|
+
*/
|
|
348
|
+
protected requireModelSession(): ModelClientSession<AstNode> {
|
|
349
|
+
if (this.disposed) {
|
|
350
|
+
throw new SessionClosedError(this.state.clientId, 'The diagram has closed; its client session has ended.');
|
|
351
|
+
}
|
|
352
|
+
const session = this.state.modelSession;
|
|
353
|
+
if (!session) {
|
|
354
|
+
throw new GLSPServerError(
|
|
355
|
+
this.sharedServices.MessageRenderer.renderMessage(DIAGRAM_SESSION_REFUSED),
|
|
356
|
+
`clientId=${this.state.clientId} has no client session: no load has registered it, or another participant holds the id`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return session;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* {@link requireModelSession} for the load, registering with the request's
|
|
364
|
+
* resume token, and telling the user when the id is refused. A load with
|
|
365
|
+
* no token waits up to {@link sessionWaitMs} for a held id to free: a
|
|
366
|
+
* client that resumes no session reconnects under its old id before the
|
|
367
|
+
* server has seen the old connection close.
|
|
368
|
+
*/
|
|
369
|
+
protected async loadModelSession(action: RequestModelAction): Promise<ModelClientSession<AstNode>> {
|
|
370
|
+
const option = action.options?.[RESUME_TOKEN_ARG];
|
|
371
|
+
const token = typeof option === 'string' ? option : undefined;
|
|
372
|
+
if (!this.state.modelSession && !this.disposed) {
|
|
373
|
+
this.state.modelSession = this.registerModelSession(token);
|
|
374
|
+
}
|
|
375
|
+
if (!this.state.modelSession && !this.disposed && token === undefined) {
|
|
376
|
+
await this.waitForSessionEnd(this.state.clientId, this.sessionWaitMs);
|
|
377
|
+
if (!this.state.modelSession && !this.disposed) {
|
|
378
|
+
this.state.modelSession = this.registerModelSession();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
return this.requireModelSession();
|
|
383
|
+
} catch (error: unknown) {
|
|
384
|
+
// A diagram closed meanwhile has no client left to tell.
|
|
385
|
+
if (isSessionClosedError(error)) {
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
// The error names the held id, and whoever ends the request logs it.
|
|
389
|
+
this.actionDispatcher
|
|
390
|
+
.dispatch(
|
|
391
|
+
MessageAction.create(this.sharedServices.MessageRenderer.renderMessage(DIAGRAM_SESSION_REFUSED), { severity: 'ERROR' })
|
|
392
|
+
)
|
|
393
|
+
.catch((dispatchError: unknown) =>
|
|
394
|
+
this.logger.warn(`Could not tell the client the diagram was refused: ${describeErrorWithStack(dispatchError)}`)
|
|
395
|
+
);
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Resolve once the client session under `clientId` ends, or after `timeoutMs`. */
|
|
401
|
+
protected waitForSessionEnd(clientId: string, timeoutMs: number): Promise<void> {
|
|
402
|
+
return new Promise<void>(resolve => {
|
|
403
|
+
const subscriptions = new DisposableCollection();
|
|
404
|
+
const done = (): void => {
|
|
405
|
+
subscriptions.dispose();
|
|
406
|
+
resolve();
|
|
407
|
+
};
|
|
408
|
+
subscriptions.push(this.sharedServices.Clock.setTimer(done, timeoutMs));
|
|
409
|
+
// A holder this service did not start is waited out by the timer.
|
|
410
|
+
const holder = this.sharedServices.model.ModelService.getSession(clientId);
|
|
411
|
+
if (holder) {
|
|
412
|
+
subscriptions.push(holder.onDidDispose(done));
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
227
417
|
/**
|
|
228
418
|
* Default load flow — {@link doLoadSourceModel}. Adopters whose load flow
|
|
229
419
|
* needs more than the seams allow override this method.
|
|
@@ -239,23 +429,29 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
239
429
|
|
|
240
430
|
// Open FIRST: a workspace-scanned-but-never-didOpened document rebuilds here,
|
|
241
431
|
// so the capture below settles on a built root rather than a transient
|
|
242
|
-
// mid-rebuild one.
|
|
243
|
-
|
|
432
|
+
// mid-rebuild one. Closed when the session ends.
|
|
433
|
+
await (await this.loadModelSession(action)).open(rootUri);
|
|
244
434
|
|
|
245
435
|
// GLSP's sessionDisposed is unreliable on Theia tab-close; dispose on client
|
|
246
436
|
// detach so reopens don't accumulate stale onModelUpdated listeners.
|
|
247
437
|
this.toDispose.push(
|
|
248
|
-
modelService.onClientClosed(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
438
|
+
modelService.onClientClosed(
|
|
439
|
+
() => {
|
|
440
|
+
this.logger.debug(`Client detached (${this.state.clientId}) — disposing storage subscriptions for ${rootUri}`);
|
|
441
|
+
this.dispose();
|
|
442
|
+
},
|
|
443
|
+
{ uri: rootUri, clientId: this.state.clientId }
|
|
444
|
+
)
|
|
252
445
|
);
|
|
253
446
|
|
|
254
447
|
// React to external rebuilds: settle-gated capture + debounced/deduped resubmit.
|
|
255
|
-
this.toDispose.push(modelService.onModelUpdated(
|
|
448
|
+
this.toDispose.push(modelService.onModelUpdated(event => this.handleModelUpdated(rootUri, event), { uri: rootUri }));
|
|
449
|
+
|
|
450
|
+
// GLSP's own command stack counts commands; tell it of another client's save.
|
|
451
|
+
this.toDispose.push(modelService.onModelSaved(event => this.handleModelSaved(event), { uri: rootUri }));
|
|
256
452
|
|
|
257
|
-
//
|
|
258
|
-
this.toDispose.push(modelService.
|
|
453
|
+
// Tell the client of each dirty flip, one the diagram did not cause included.
|
|
454
|
+
this.toDispose.push(modelService.onDirtyChanged(event => this.handleDirtyChanged(event)));
|
|
259
455
|
|
|
260
456
|
// Capture the initial settled root.
|
|
261
457
|
const document = await modelService.settled(rootUri);
|
|
@@ -263,20 +459,33 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
263
459
|
}
|
|
264
460
|
|
|
265
461
|
/**
|
|
266
|
-
* Capture the initial settled root and
|
|
267
|
-
*
|
|
268
|
-
* the first {@link handleModelUpdated} (at `Validated`, with diagnostics) flips
|
|
269
|
-
* it READONLY if the document has structural parse errors. Edit-mode +
|
|
270
|
-
* settle-hook actions are dispatched on a macrotask so the initial
|
|
462
|
+
* Capture the initial settled root and its parse-error status. Settle-hook
|
|
463
|
+
* actions are dispatched on a macrotask so the initial
|
|
271
464
|
* `requestModel → setModel` handshake isn't perturbed.
|
|
465
|
+
*
|
|
466
|
+
* The initial status is decided from the root's own parse (see
|
|
467
|
+
* {@link isStructurallyBroken}), not from diagnostics: `settled()` can hand
|
|
468
|
+
* back a document it drove to the integrity landmark, before validation,
|
|
469
|
+
* with no diagnostics yet.
|
|
272
470
|
*/
|
|
273
471
|
protected async captureSettledRoot(rootUri: string, document: AstDocument<AstNode, never>): Promise<void> {
|
|
274
|
-
this.state.setSourceRoot(rootUri, document.root as TRoot);
|
|
275
|
-
|
|
472
|
+
await this.state.runExclusive(() => this.state.setSourceRoot(rootUri, document.root as TRoot));
|
|
473
|
+
this.refreshParseErrorStatus(document);
|
|
474
|
+
const actions = this.onSourceModelSettled(document);
|
|
276
475
|
if (actions.length > 0) {
|
|
277
476
|
// Defer through the injectable Clock (not raw setTimeout) so the dispatch is
|
|
278
477
|
// testable and cancels on session disposal; parked on toDispose for that.
|
|
279
|
-
this.toDispose.push(
|
|
478
|
+
this.toDispose.push(
|
|
479
|
+
this.sharedServices.Clock.setTimer(
|
|
480
|
+
() =>
|
|
481
|
+
this.actionDispatcher
|
|
482
|
+
.dispatchAll(actions)
|
|
483
|
+
.catch((error: unknown) =>
|
|
484
|
+
this.logger.warn(`Could not send the settled model of ${rootUri}: ${describeErrorWithStack(error)}`)
|
|
485
|
+
),
|
|
486
|
+
0
|
|
487
|
+
)
|
|
488
|
+
);
|
|
280
489
|
}
|
|
281
490
|
}
|
|
282
491
|
|
|
@@ -303,87 +512,106 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
303
512
|
if (!current.has(uri)) {
|
|
304
513
|
subscription.dispose();
|
|
305
514
|
this.secondarySubscriptions.delete(uri);
|
|
515
|
+
this.departedSecondaries.add(uri);
|
|
306
516
|
}
|
|
307
517
|
}
|
|
308
518
|
for (const uri of current) {
|
|
309
519
|
if (!this.secondarySubscriptions.has(uri)) {
|
|
520
|
+
this.departedSecondaries.delete(uri);
|
|
310
521
|
this.secondarySubscriptions.set(
|
|
311
522
|
uri,
|
|
312
|
-
this.sharedServices.model.ModelService.onModelUpdated(
|
|
523
|
+
this.sharedServices.model.ModelService.onModelUpdated(event => this.handleSecondaryUpdated(uri, event), { uri })
|
|
313
524
|
);
|
|
525
|
+
this.openSecondary(uri);
|
|
314
526
|
}
|
|
315
527
|
}
|
|
316
528
|
}
|
|
317
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Open a document that joined the write set through the diagram's session,
|
|
532
|
+
* where it stays open until the diagram's next save after it leaves the set,
|
|
533
|
+
* or the diagram's end. A document that does not exist yet is left to the
|
|
534
|
+
* write that creates it, and a failed open is logged: the write that needs
|
|
535
|
+
* the document opens it again and fails there.
|
|
536
|
+
*/
|
|
537
|
+
protected openSecondary(uri: string): void {
|
|
538
|
+
const session = this.state.modelSession;
|
|
539
|
+
if (!session || !this.sharedServices.model.ModelService.getDocument(uri)) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
session.open(uri).catch((error: unknown) => {
|
|
543
|
+
const detail = `Could not open ${uri} for ${this.state.clientId}: ${describeError(error)}`;
|
|
544
|
+
// A session ending while the open reads is the diagram closing.
|
|
545
|
+
if (isSessionClosedError(error)) {
|
|
546
|
+
this.logger.debug(detail);
|
|
547
|
+
} else {
|
|
548
|
+
this.logger.warn(detail);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
318
553
|
/**
|
|
319
554
|
* React to a rebuild of a SECONDARY document by resubmitting the diagram, so
|
|
320
555
|
* an external edit to (say) a layout file reaches the canvas.
|
|
321
556
|
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
557
|
+
* Applies {@link handleModelUpdated}'s guard, and that is the load-bearing
|
|
558
|
+
* half rather than the resubmit: a diagram interaction that writes a
|
|
324
559
|
* secondary — a drag persisting bounds to a layout file — comes back through
|
|
325
|
-
* this listener
|
|
560
|
+
* this listener caused by the client's own write, and resubmitting on it
|
|
326
561
|
* fights the optimistic client-side move the user is still holding.
|
|
327
562
|
*
|
|
328
|
-
* Schedules the PRIMARY's current document, never the secondary's.
|
|
329
|
-
* {@link doUpdateAndSubmit} reads its event document only for the edit-mode
|
|
330
|
-
* and structurally-broken decisions, both of which are statements about the
|
|
331
|
-
* document the canvas edits: passing the secondary's would let a typo in a
|
|
332
|
-
* layout file flip the canvas READONLY and take the keep-the-last-valid-GModel
|
|
333
|
-
* branch off an unrelated document's parse state. The pending-document field
|
|
334
|
-
* is shared with the primary path, so it would poison a concurrently
|
|
335
|
-
* scheduled primary resubmit too.
|
|
336
|
-
*
|
|
337
563
|
* No marker refresh, unlike the primary path: a secondary's diagnostics reach
|
|
338
564
|
* the client only if an adopter's index registers elements as rendering that
|
|
339
565
|
* document, and refreshing here unconditionally would add a dispatch to every
|
|
340
566
|
* drag this client authors — the guard above gates the resubmit, not the
|
|
341
567
|
* markers.
|
|
342
568
|
*/
|
|
343
|
-
protected handleSecondaryUpdated(uri: string, event: AstDocumentUpdatedEvent<AstNode
|
|
569
|
+
protected handleSecondaryUpdated(uri: string, event: AstDocumentUpdatedEvent<AstNode>): void {
|
|
344
570
|
if (this.disposeIfStale(uri)) {
|
|
345
571
|
return;
|
|
346
572
|
}
|
|
347
|
-
if (this.state.clientId
|
|
573
|
+
if (event.causedBy === this.state.clientId || this.currentPrimaryDocument() === undefined) {
|
|
348
574
|
return;
|
|
349
575
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
this.logger.debug(`Secondary ${uri} rebuilt by ${event.sourceClientId} (${event.reason}) — scheduling resubmit`);
|
|
355
|
-
this.scheduleUpdateAndSubmit(primary);
|
|
576
|
+
this.logger.debug(
|
|
577
|
+
`Secondary ${uri} rebuilt, caused by ${event.causedBy ?? 'an unknown client'} (${event.reason}) — scheduling resubmit`
|
|
578
|
+
);
|
|
579
|
+
this.scheduleUpdateAndSubmit();
|
|
356
580
|
}
|
|
357
581
|
|
|
358
582
|
/**
|
|
359
583
|
* The primary document's current state, or `undefined` when it is not
|
|
360
584
|
* registered.
|
|
361
585
|
*
|
|
362
|
-
* Synchronous by construction
|
|
363
|
-
*
|
|
364
|
-
* `settled()` is not an alternative: it strips diagnostics by phase contract,
|
|
365
|
-
* and diagnostics are the reason {@link doUpdateAndSubmit} takes a separate
|
|
366
|
-
* event document at all.
|
|
586
|
+
* Synchronous by construction, so the parse-error status reads the text as
|
|
587
|
+
* it stands rather than waiting for a build.
|
|
367
588
|
*/
|
|
368
|
-
protected currentPrimaryDocument(): AstDocument<AstNode
|
|
589
|
+
protected currentPrimaryDocument(): AstDocument<AstNode> | undefined {
|
|
369
590
|
const document = this.sharedServices.model.ModelService.getDocument(this.state.sourceUri);
|
|
370
|
-
return document ?
|
|
591
|
+
return document ? this.sharedServices.workspace.AstDocumentManager.toAstDocument(document) : undefined;
|
|
371
592
|
}
|
|
372
593
|
|
|
373
594
|
/**
|
|
374
595
|
* React to an external rebuild reaching `Validated`. Self-cleans if the
|
|
375
596
|
* session has vanished but this listener leaked (neither `sessionDisposed`
|
|
376
|
-
* nor `onClientClosed` fired on tab close). Resubmits for any
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
597
|
+
* nor `onClientClosed` fired on tab close). Resubmits for any update this
|
|
598
|
+
* client's own write did not cause alone, then refreshes diagnostic markers
|
|
599
|
+
* for every update — including own writes, which skip the resubmit but can
|
|
600
|
+
* still change diagnostics. An event without `causedBy` is resubmitted.
|
|
601
|
+
*
|
|
602
|
+
* Own writes include a primary that the build of a write to a secondary
|
|
603
|
+
* swept in, which arrives `rebuilt`. Skipping it loses nothing the canvas
|
|
604
|
+
* shows: the operation behind the write submits once the write has
|
|
605
|
+
* answered, and a build steps its whole batch through each phase, so that
|
|
606
|
+
* submit already renders the relinked primary. What the later `Validated`
|
|
607
|
+
* adds are diagnostics, which the marker refresh carries.
|
|
380
608
|
*/
|
|
381
|
-
protected async handleModelUpdated(rootUri: string, event: AstDocumentUpdatedEvent<AstNode
|
|
609
|
+
protected async handleModelUpdated(rootUri: string, event: AstDocumentUpdatedEvent<AstNode>): Promise<void> {
|
|
382
610
|
if (this.disposeIfStale(rootUri)) {
|
|
383
611
|
return;
|
|
384
612
|
}
|
|
385
|
-
if (
|
|
386
|
-
this.scheduleUpdateAndSubmit(
|
|
613
|
+
if (event.causedBy !== this.state.clientId) {
|
|
614
|
+
this.scheduleUpdateAndSubmit();
|
|
387
615
|
}
|
|
388
616
|
await this.refreshDiagnosticMarkers();
|
|
389
617
|
}
|
|
@@ -400,72 +628,127 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
400
628
|
if (this.sessionManager.getSession(this.state.clientId)) {
|
|
401
629
|
return false;
|
|
402
630
|
}
|
|
403
|
-
this.logger.
|
|
631
|
+
this.logger.debug(`Stale storage (clientId=${this.state.clientId} not in ClientSessionManager) — self-disposing ${context}`);
|
|
404
632
|
this.dispose();
|
|
405
633
|
return true;
|
|
406
634
|
}
|
|
407
635
|
|
|
408
636
|
/**
|
|
409
|
-
*
|
|
410
|
-
* {@link
|
|
411
|
-
* {@link doUpdateAndSubmit} once with the most recent document.
|
|
637
|
+
* (Re)arm the trailing-edge {@link resubmitDebouncer}, so a burst of
|
|
638
|
+
* rebuilds runs {@link doUpdateAndSubmit} once.
|
|
412
639
|
*/
|
|
413
|
-
protected scheduleUpdateAndSubmit(
|
|
414
|
-
this.pendingResubmitDocument = document;
|
|
640
|
+
protected scheduleUpdateAndSubmit(): void {
|
|
415
641
|
this.resubmitDebouncer.schedule();
|
|
416
642
|
}
|
|
417
643
|
|
|
418
644
|
/**
|
|
419
|
-
* Debouncer callback: run the resubmit
|
|
420
|
-
*
|
|
421
|
-
*
|
|
645
|
+
* Debouncer callback: run the resubmit against this storage's (invariant)
|
|
646
|
+
* source URI and dispatch the result. Fire-and-forget — failures are
|
|
647
|
+
* logged, not surfaced to the timer caller.
|
|
422
648
|
*/
|
|
423
649
|
protected flushResubmit(): void {
|
|
424
|
-
const document = this.pendingResubmitDocument;
|
|
425
|
-
if (document === undefined) {
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
650
|
const rootUri = this.state.sourceUri;
|
|
429
|
-
this.doUpdateAndSubmit(rootUri
|
|
430
|
-
actions =>
|
|
431
|
-
|
|
651
|
+
this.doUpdateAndSubmit(rootUri).then(
|
|
652
|
+
actions =>
|
|
653
|
+
this.actionDispatcher
|
|
654
|
+
.dispatchAll(actions)
|
|
655
|
+
.catch((error: unknown) =>
|
|
656
|
+
this.logger.warn(`Could not send the resubmitted model of ${rootUri}: ${describeErrorWithStack(error)}`)
|
|
657
|
+
),
|
|
658
|
+
error => this.logger.error(`Update-and-submit failed for ${rootUri}: ${describeErrorWithStack(error)}`)
|
|
432
659
|
);
|
|
433
660
|
}
|
|
434
661
|
|
|
435
662
|
/**
|
|
436
|
-
*
|
|
437
|
-
* mark
|
|
438
|
-
*
|
|
663
|
+
* For a diagram module on GLSP's own command stack, which counts commands:
|
|
664
|
+
* mark it clean when another client persists the document.
|
|
665
|
+
* `HydraniumGlspCommandStack` reads the text store instead, and this does
|
|
666
|
+
* not move it.
|
|
439
667
|
*/
|
|
440
|
-
protected handleModelSaved(event: AstDocumentSavedEvent<AstNode
|
|
668
|
+
protected handleModelSaved(event: AstDocumentSavedEvent<AstNode>): void {
|
|
441
669
|
if (this.state.clientId !== event.sourceClientId) {
|
|
442
670
|
this.commandStack.saveIsDone();
|
|
443
671
|
}
|
|
444
672
|
}
|
|
445
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Send the diagram's dirty state when a document it has open turns dirty or
|
|
676
|
+
* clean, the answer being the command stack's. GLSP sends it only with a
|
|
677
|
+
* model submission or a save of the diagram's own, so a save by another
|
|
678
|
+
* client, an editor included, or an edit that changes nothing the diagram
|
|
679
|
+
* shows would otherwise leave the client's marker wrong until the next
|
|
680
|
+
* gesture. A flip the diagram's own operation causes is sent twice, once
|
|
681
|
+
* here and once with the submission, and the client keeps the one state.
|
|
682
|
+
*
|
|
683
|
+
* Nothing is sent while a save of the diagram's own is awaited: GLSP's save
|
|
684
|
+
* handler sends the state once the save settles, reason `save`, and GLSP's
|
|
685
|
+
* client keeps a state only when it changes, so the save's own flip sent
|
|
686
|
+
* here first would leave that answer changing nothing. GLSP's saveable
|
|
687
|
+
* waits for exactly that answer, and times out without it.
|
|
688
|
+
*/
|
|
689
|
+
protected handleDirtyChanged(event: ModelDirtyChangedEvent): void {
|
|
690
|
+
if (this.ownSavesPending > 0 || !this.sharedServices.workspace.TextDocuments.isOpenInClient(event.uri, this.state.clientId)) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
this.sendDirtyState();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Send the command stack's dirty state, reason `external`. */
|
|
697
|
+
protected sendDirtyState(): void {
|
|
698
|
+
this.actionDispatcher
|
|
699
|
+
.dispatch(SetDirtyStateAction.create(this.commandStack.isDirty, { reason: 'external' }))
|
|
700
|
+
.catch((error: unknown) =>
|
|
701
|
+
this.logger.warn(`Could not send the dirty state of ${this.state.sourceUri}: ${describeErrorWithStack(error)}`)
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
446
705
|
/**
|
|
447
706
|
* Re-settle to a guaranteed fully-linked + reprojected root, capture it, and
|
|
448
|
-
* (unless suppressed) resubmit a deduped external GModel.
|
|
449
|
-
*
|
|
450
|
-
*
|
|
707
|
+
* (unless suppressed) resubmit a deduped external GModel.
|
|
708
|
+
*
|
|
709
|
+
* The settle is awaited outside {@link AbstractHydraniumGlspState.runExclusive},
|
|
710
|
+
* so an operation does not wait on a build; the capture and the render run
|
|
711
|
+
* inside it, the render's own wait for the document included, since an
|
|
712
|
+
* operation opened between that wait and the GModel build would be rendered.
|
|
451
713
|
*/
|
|
452
|
-
protected async doUpdateAndSubmit(rootUri: string
|
|
453
|
-
// Settle-gate the capture: never setSourceRoot off the event's possibly-transient
|
|
454
|
-
// snapshot — the event can arrive while a re-entered document is mid-rebuild.
|
|
714
|
+
protected async doUpdateAndSubmit(rootUri: string): Promise<Action[]> {
|
|
455
715
|
const document = await this.sharedServices.model.ModelService.settled(rootUri);
|
|
456
|
-
this.state.
|
|
457
|
-
|
|
716
|
+
return this.state.runExclusive(() => this.captureAndSubmit(rootUri, document.root as TRoot));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* The part of {@link doUpdateAndSubmit} that runs inside the boundary.
|
|
721
|
+
* `root` is not captured when the state already holds a later one: an
|
|
722
|
+
* operation that ended while the settle was awaited captured its own
|
|
723
|
+
* write, which `root` predates.
|
|
724
|
+
*
|
|
725
|
+
* The parse-error status is the primary's as it stands now, which can be
|
|
726
|
+
* past `root`: a status taken from an older document would let an operation
|
|
727
|
+
* write into text that no longer parses.
|
|
728
|
+
*/
|
|
729
|
+
protected async captureAndSubmit(rootUri: string, root: TRoot): Promise<Action[]> {
|
|
730
|
+
if (this.sharedServices.workspace.ModelLedger.versionOf(root) >= this.state.version) {
|
|
731
|
+
this.state.setSourceRoot(rootUri, root);
|
|
732
|
+
}
|
|
733
|
+
const primary = this.currentPrimaryDocument();
|
|
734
|
+
if (!primary) {
|
|
735
|
+
// A primary deleted since the settle leaves nothing to be broken.
|
|
736
|
+
this.state.setStatus(DiagramStatus.PARSE_ERROR, undefined);
|
|
737
|
+
}
|
|
738
|
+
const broken = primary ? this.refreshParseErrorStatus(primary) : [];
|
|
458
739
|
|
|
459
740
|
// Skip the external submit until the initial requestModel completes; submitting too
|
|
460
741
|
// early bumps root.revision and the client's stale first computedBounds is dropped,
|
|
461
742
|
// leaving the canvas empty.
|
|
462
743
|
if (this.submissionHandler.hasPendingInitialRequest()) {
|
|
463
|
-
return
|
|
744
|
+
return [];
|
|
464
745
|
}
|
|
465
|
-
// While the
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
746
|
+
// While a document of the write set is structurally broken, keep the GModel the
|
|
747
|
+
// client has, which the initial request has delivered by now, so the canvas
|
|
748
|
+
// doesn't blank or lose its layout mid-typing. The parse-error status still
|
|
749
|
+
// makes it read-only.
|
|
750
|
+
if (broken.length > 0) {
|
|
751
|
+
return [];
|
|
469
752
|
}
|
|
470
753
|
// Read the handler's signature BEFORE submitting and again after: it records
|
|
471
754
|
// every submission whatever the reason, so an unmoved signature means this
|
|
@@ -474,50 +757,76 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
474
757
|
// to be answered by an echo of itself — the operation had delivered the
|
|
475
758
|
// model, and the external comparison had never heard of it.
|
|
476
759
|
const previousSignature = this.submissionHandler.lastSubmittedSignature;
|
|
760
|
+
// The number this submission takes as it starts.
|
|
761
|
+
const submission = this.submissionHandler.startedSubmissions + 1;
|
|
477
762
|
const submitActions = await this.submissionHandler.submitModel('external');
|
|
478
763
|
if (this.submissionHandler.lastSubmittedSignature === previousSignature) {
|
|
479
|
-
|
|
764
|
+
this.submissionHandler.withdrawLastSubmission(submission);
|
|
765
|
+
return [];
|
|
480
766
|
}
|
|
481
|
-
|
|
482
|
-
return [...submitActions, ...editModeActions];
|
|
767
|
+
return submitActions;
|
|
483
768
|
}
|
|
484
769
|
|
|
485
770
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
771
|
+
* Set {@link DiagramStatus.PARSE_ERROR} to {@link parseErrorStatus} while any
|
|
772
|
+
* document the diagram writes is structurally broken, and withdraw it once
|
|
773
|
+
* they all parse. Answers the documents that are broken.
|
|
774
|
+
*
|
|
775
|
+
* The secondaries count as much as the primary: an operation writes them from
|
|
776
|
+
* the AST their parse recovered, which drops what the parser skipped, so a
|
|
777
|
+
* write into a broken secondary overwrites the user's text.
|
|
489
778
|
*/
|
|
490
|
-
protected
|
|
491
|
-
const broken = this.isStructurallyBroken(document);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
779
|
+
protected refreshParseErrorStatus(primary: AstDocument<AstNode>): AstDocument<AstNode>[] {
|
|
780
|
+
const broken = [primary, ...this.secondaryDocuments(primary.uri)].filter(document => this.isStructurallyBroken(document));
|
|
781
|
+
this.state.setStatus(DiagramStatus.PARSE_ERROR, broken.length > 0 ? this.parseErrorStatus(broken) : undefined);
|
|
782
|
+
return broken;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** The write-set secondaries that exist, other than `primaryUri`. */
|
|
786
|
+
protected secondaryDocuments(primaryUri: string): AstDocument<AstNode>[] {
|
|
787
|
+
const { ModelService } = this.sharedServices.model;
|
|
788
|
+
return this.state.secondaryUris
|
|
789
|
+
.filter(uri => uri !== primaryUri)
|
|
790
|
+
.map(uri => ModelService.getDocument(uri))
|
|
791
|
+
.filter(document => document !== undefined)
|
|
792
|
+
.map(document => this.sharedServices.workspace.AstDocumentManager.toAstDocument(document));
|
|
498
793
|
}
|
|
499
794
|
|
|
500
795
|
/**
|
|
501
796
|
* Seam: is the AST structurally broken (lexing/parsing errors)? Drives both
|
|
502
|
-
*
|
|
503
|
-
* instead of blanking it mid-typing.
|
|
504
|
-
*
|
|
505
|
-
*
|
|
797
|
+
* the {@link DiagramStatus.PARSE_ERROR} status and the GModel resubmit
|
|
798
|
+
* skip — keeping the last valid canvas instead of blanking it mid-typing.
|
|
799
|
+
* Default: the lexer and parser errors of
|
|
800
|
+
* the root's own parse, which exist from the parse on, so the answer does
|
|
801
|
+
* not wait for validation; for a root its document has since replaced, any
|
|
802
|
+
* error-severity diagnostic carrying a Langium `lexing-error` /
|
|
803
|
+
* `parsing-error` code. Adopters override to compose additional break
|
|
804
|
+
* reasons.
|
|
506
805
|
*/
|
|
507
|
-
protected isStructurallyBroken(document: AstDocument<AstNode
|
|
508
|
-
|
|
806
|
+
protected isStructurallyBroken(document: AstDocument<AstNode>): boolean {
|
|
807
|
+
const parsed = document.root.$document?.parseResult;
|
|
808
|
+
if (parsed?.value === document.root) {
|
|
809
|
+
return hasStructuralErrors(parsed);
|
|
810
|
+
}
|
|
811
|
+
return document.diagnostics?.some(diagnostic => isStructuralDiagnostic(diagnostic)) ?? false;
|
|
509
812
|
}
|
|
510
813
|
|
|
511
814
|
/**
|
|
512
|
-
* Seam:
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
815
|
+
* Seam: the {@link DiagramStatus.PARSE_ERROR} status while `brokenDocuments`
|
|
816
|
+
* do not parse, the primary first when it is among them. Default: a read-only
|
|
817
|
+
* error whose {@link DIAGRAM_READONLY_PARSE_ERROR} message names the first.
|
|
818
|
+
*
|
|
819
|
+
* A read-only status needs a message. Read-only withdraws the tool palette,
|
|
820
|
+
* and without a message nothing on the canvas says why.
|
|
518
821
|
*/
|
|
519
|
-
protected
|
|
520
|
-
return
|
|
822
|
+
protected parseErrorStatus(brokenDocuments: AstDocument<AstNode>[]): DiagramStatusEntry {
|
|
823
|
+
return {
|
|
824
|
+
message: this.sharedServices.MessageRenderer.renderMessage(DIAGRAM_READONLY_PARSE_ERROR, {
|
|
825
|
+
document: UriUtils.basename(URI.parse(brokenDocuments[0].uri))
|
|
826
|
+
}),
|
|
827
|
+
severity: 'ERROR',
|
|
828
|
+
readonly: true
|
|
829
|
+
};
|
|
521
830
|
}
|
|
522
831
|
|
|
523
832
|
/**
|
|
@@ -528,32 +837,49 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
528
837
|
return [];
|
|
529
838
|
}
|
|
530
839
|
|
|
531
|
-
/** The effective {@link
|
|
532
|
-
protected get
|
|
533
|
-
return this.
|
|
840
|
+
/** The effective {@link SaveDeliveryPolicy}: the bound option, or {@link DEFAULT_SAVE_DELIVERY_POLICY} when unbound. */
|
|
841
|
+
protected get saveDeliveryPolicy(): SaveDeliveryPolicy {
|
|
842
|
+
return this.boundSaveDeliveryPolicy ?? DEFAULT_SAVE_DELIVERY_POLICY;
|
|
534
843
|
}
|
|
535
844
|
|
|
536
845
|
/**
|
|
537
|
-
* Default save flow:
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
846
|
+
* Default save flow: persist the store's current text for every document the
|
|
847
|
+
* diagram's session has open.
|
|
848
|
+
*
|
|
849
|
+
* **A save persists, it does not author.** Every diagram gesture already
|
|
850
|
+
* reached the store through the diagram's session, so the store holds the
|
|
851
|
+
* settled text and disk is the only thing behind. Re-serializing from the AST
|
|
852
|
+
* here cannot improve on that text and can only damage it: a serializer
|
|
853
|
+
* normalises formatting, and the comments the write path carries over keep
|
|
854
|
+
* none of the hand-formatting around them, so a save would reflow a document
|
|
855
|
+
* nothing changed — which is what a pure bounds drag does to the semantic
|
|
856
|
+
* file when only its layout moved.
|
|
857
|
+
*
|
|
858
|
+
* **What is saved is the primary, every tracked secondary
|
|
859
|
+
* (`AbstractHydraniumGlspState.trackSecondaryDocument`) and every document
|
|
860
|
+
* that left the write set since the last save**, so a document the diagram
|
|
861
|
+
* wrote is persisted whether or not it is the one the client named. One the
|
|
862
|
+
* diagram's session does not have open is skipped, even when another client
|
|
863
|
+
* has it open: that client's unsaved edits are not the diagram's to persist.
|
|
864
|
+
*
|
|
865
|
+
* **A save therefore names documents the gesture did not aim at**, which is
|
|
866
|
+
* why an unchanged one is not rewritten even though the user asked for a
|
|
867
|
+
* save: the mtime would move on a file they never touched. With the default
|
|
868
|
+
* session, `AstDocumentManager.save` decides that per document and
|
|
869
|
+
* announces the save either way.
|
|
542
870
|
*
|
|
543
|
-
* The configured {@link
|
|
544
|
-
* decides
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
* - `drop-and-log`: guard, fire-and-forget, log + swallow failures — a stale
|
|
550
|
-
* diagram save is dropped because a concurrent form/code edit already wrote
|
|
551
|
-
* the truth, and GLSP exposes no save-failure back-channel.
|
|
871
|
+
* The configured {@link SaveDeliveryPolicy} (see {@link saveDeliveryPolicy})
|
|
872
|
+
* decides await-vs-fire-and-forget and failure handling. The save persists
|
|
873
|
+
* at `'any'`: the base-version guard exists to stop a stale writer
|
|
874
|
+
* overwriting a newer document, and a flush persists the store — which
|
|
875
|
+
* already holds every other client's change, including the one that
|
|
876
|
+
* advanced the version.
|
|
552
877
|
*
|
|
553
878
|
* Returns `MaybePromise<void>` to match upstream `SourceModelStorage`:
|
|
554
|
-
* resolves with the
|
|
555
|
-
*
|
|
556
|
-
* `
|
|
879
|
+
* resolves with the flush under `await`, returns synchronously under
|
|
880
|
+
* `fire-and-forget`. To change how each document is written, override the
|
|
881
|
+
* session class's `persistDocument`; an override of this method that writes
|
|
882
|
+
* past the session skips that override.
|
|
557
883
|
*/
|
|
558
884
|
saveSourceModel(action: SaveModelAction): MaybePromise<void> {
|
|
559
885
|
// Normalised for the same reason the load path is: `SaveModelAction.fileUri`
|
|
@@ -563,24 +889,92 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
563
889
|
// the call site rather than inside `getFileUri` so that method's contract —
|
|
564
890
|
// return what the action said — is unchanged for adopters overriding it.
|
|
565
891
|
const uri = this.toSourceModelUri(this.getFileUri(action));
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
baseVersion: policy.kind === 'overwrite' ? undefined : this.state.version
|
|
572
|
-
}).then(() => undefined);
|
|
573
|
-
|
|
574
|
-
if (policy.kind === 'drop-and-log') {
|
|
575
|
-
// Fire-and-forget: the diagram save lost the race to a concurrent edit
|
|
576
|
-
// that already persisted the truth, so it must neither block the action
|
|
577
|
-
// nor surface. Log rather than leave an unhandled rejection.
|
|
578
|
-
persisted.catch(error => this.logger.error(`Save failed for ${uri}: ${error instanceof Error ? error.message : String(error)}`));
|
|
892
|
+
if (this.saveDeliveryPolicy.kind === 'fire-and-forget') {
|
|
893
|
+
// Log rather than leave an unhandled rejection: the promise is not
|
|
894
|
+
// returned, so nothing else will observe a failure. Named for the
|
|
895
|
+
// diagram, since the document that failed may be a secondary.
|
|
896
|
+
this.flushWriteSet(uri).catch(error => this.logger.error(`Diagram save failed for ${uri}: ${describeErrorWithStack(error)}`));
|
|
579
897
|
return undefined;
|
|
580
898
|
}
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
|
|
899
|
+
// Awaited: any failure propagates to GLSP's save-action handler, which
|
|
900
|
+
// then sends no dirty state, so the one the save held back goes here.
|
|
901
|
+
// An override that throws before it returns a promise throws inside the
|
|
902
|
+
// async function too, so the hold always ends.
|
|
903
|
+
this.ownSavesPending++;
|
|
904
|
+
return (async () => {
|
|
905
|
+
try {
|
|
906
|
+
await this.flushWriteSet(uri);
|
|
907
|
+
} catch (error: unknown) {
|
|
908
|
+
this.sendDirtyState();
|
|
909
|
+
throw error;
|
|
910
|
+
} finally {
|
|
911
|
+
this.ownSavesPending--;
|
|
912
|
+
}
|
|
913
|
+
})();
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Save the stored text of `primaryUri`, every tracked secondary and every
|
|
918
|
+
* document that left the write set since the last save, each only where
|
|
919
|
+
* the diagram's session has it open; then close those that left. Each goes
|
|
920
|
+
* through the session's `persist`, so an override of the session class's
|
|
921
|
+
* `persistDocument` applies to diagram saves too. Each saved document is
|
|
922
|
+
* then settled in the language client, so the save does not answer before
|
|
923
|
+
* that client has answered the push of what it wrote.
|
|
924
|
+
*
|
|
925
|
+
* Deduplicated, because a state that tracks its own primary as a secondary
|
|
926
|
+
* would otherwise save it twice and fire two save notifications for one
|
|
927
|
+
* save. Every save is called in one synchronous step, so each takes its text
|
|
928
|
+
* before anything can close a document of the set: saved one after another,
|
|
929
|
+
* a GLSP session ending during the first write closes the rest unsaved. A
|
|
930
|
+
* `persistDocument` override that awaits before calling the base gives that
|
|
931
|
+
* up, and a session ending during its await fails the rest with
|
|
932
|
+
* `DocumentNotOpenError`.
|
|
933
|
+
*/
|
|
934
|
+
protected async flushWriteSet(primaryUri: string): Promise<void> {
|
|
935
|
+
// Refused like the load: without a session the client id's opens are
|
|
936
|
+
// another participant's, and this would save them.
|
|
937
|
+
const session = this.requireModelSession();
|
|
938
|
+
const textDocuments = this.sharedServices.workspace.TextDocuments;
|
|
939
|
+
const clientId = this.state.clientId;
|
|
940
|
+
// Taken with the targets: a document leaving the set during the writes
|
|
941
|
+
// was not saved by them, and stays open for the next save.
|
|
942
|
+
const departed = [...this.departedSecondaries];
|
|
943
|
+
const targets = [...new Set([primaryUri, ...this.state.secondaryUris, ...departed])].filter(uri =>
|
|
944
|
+
textDocuments.isOpenInClient(uri, clientId)
|
|
945
|
+
);
|
|
946
|
+
// Async, so a persist that throws instead of rejecting fails its own
|
|
947
|
+
// document rather than leaving the rest of the set uncalled. Settled in
|
|
948
|
+
// full before failing, or the save's dirty-state hold ends while the
|
|
949
|
+
// rest are still writing.
|
|
950
|
+
const modelService = this.sharedServices.model.ModelService;
|
|
951
|
+
const results = await Promise.allSettled(
|
|
952
|
+
targets.map(async target => {
|
|
953
|
+
await session.persist({ uri: target, baseVersion: 'any' });
|
|
954
|
+
await modelService.settleLanguageClient(target);
|
|
955
|
+
})
|
|
956
|
+
);
|
|
957
|
+
const failures = results.flatMap((result, i) => (result.status === 'rejected' ? [{ uri: targets[i], reason: result.reason }] : []));
|
|
958
|
+
if (failures.length > 0) {
|
|
959
|
+
// A persist's error need not name its document, so the save's names each one.
|
|
960
|
+
const named = failures.map(({ uri, reason }) => `${uri} (${describeError(reason)})`).join(', ');
|
|
961
|
+
throw new Error(`Save failed for ${named}`, { cause: failures[0].reason });
|
|
962
|
+
}
|
|
963
|
+
for (const uri of departed) {
|
|
964
|
+
if (this.departedSecondaries.delete(uri) && uri !== this.state.sourceUri) {
|
|
965
|
+
try {
|
|
966
|
+
await this.state.modelSession?.close(uri);
|
|
967
|
+
} catch (error: unknown) {
|
|
968
|
+
// A session that ended during the writes closed everything it
|
|
969
|
+
// had open; refusing the close must not fail a save that wrote.
|
|
970
|
+
// Caught around the call, since an ended session throws
|
|
971
|
+
// before it returns a promise.
|
|
972
|
+
if (!isSessionClosedError(error)) {
|
|
973
|
+
throw error;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
584
978
|
}
|
|
585
979
|
|
|
586
980
|
/**
|
|
@@ -616,16 +1010,19 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
616
1010
|
* A model request DOES carry a request id, so a throw here takes the
|
|
617
1011
|
* client-request path rather than the toast path — and on that path `detail`
|
|
618
1012
|
* comes from `cause?.toString?.()`. A single-argument throw therefore reaches
|
|
619
|
-
* neither the
|
|
620
|
-
* are addressed separately, as on the save path: `message`
|
|
621
|
-
* in the user's terms, `cause` names the
|
|
622
|
-
* missing.
|
|
1013
|
+
* neither the server log nor the client console: both print `undefined`. So
|
|
1014
|
+
* the two readers are addressed separately, as on the save path: `message`
|
|
1015
|
+
* names what failed in the user's terms and is rendered, `cause` names the
|
|
1016
|
+
* action and the option key that was missing and stays English. On THIS path
|
|
1017
|
+
* the cause reaches only logs — `RejectAction.detail` is read by nothing but
|
|
1018
|
+
* the client action-dispatcher's `logger.warn` — which is not what makes it
|
|
1019
|
+
* English; its content is.
|
|
623
1020
|
*/
|
|
624
1021
|
protected getSourceUri(action: RequestModelAction): string {
|
|
625
1022
|
const sourceUri = action.options?.[SOURCE_URI_ARG];
|
|
626
1023
|
if (typeof sourceUri !== 'string') {
|
|
627
1024
|
throw new GLSPServerError(
|
|
628
|
-
|
|
1025
|
+
this.sharedServices.MessageRenderer.renderMessage(SOURCE_URI_MISSING),
|
|
629
1026
|
`no '${SOURCE_URI_ARG}' option on the ${action.kind} action (received ${typeof sourceUri})`
|
|
630
1027
|
);
|
|
631
1028
|
}
|
|
@@ -639,18 +1036,24 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
639
1036
|
* {@link AbstractHydraniumGlspState.setSourceRoot}.
|
|
640
1037
|
*
|
|
641
1038
|
* A save action carries no request id, so a throw here reaches the user as a
|
|
642
|
-
* toast built from `message`,
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
1039
|
+
* toast built from `message`, while `cause` travels in
|
|
1040
|
+
* `MessageAction.details` — **which a Theia host DOES put in front of a
|
|
1041
|
+
* user**, as a "Show details" button on the toast opening a dialog. So the
|
|
1042
|
+
* two are not split by reachability; they are split by AUDIENCE, which is
|
|
1043
|
+
* the older rule and the one that holds here. The message names what failed
|
|
1044
|
+
* in the user's terms and is rendered; the cause names which lookups came
|
|
1045
|
+
* back empty for whom, and stays English because a wire action kind, an
|
|
1046
|
+
* option key and a client id address whoever composes the system — a
|
|
1047
|
+
* translated developer string is a worse outcome than an untranslated one.
|
|
1048
|
+
* Neither can name the URI — not having one IS the failure, and both sources
|
|
1049
|
+
* are written together, so the typed `sourceUri` is equally empty whenever
|
|
1050
|
+
* this fires.
|
|
648
1051
|
*/
|
|
649
1052
|
protected getFileUri(action: SaveModelAction): string {
|
|
650
1053
|
const uri = action.fileUri ?? this.state.get<string>(SOURCE_URI_ARG);
|
|
651
1054
|
if (!uri) {
|
|
652
1055
|
throw new GLSPServerError(
|
|
653
|
-
|
|
1056
|
+
this.sharedServices.MessageRenderer.renderMessage(SAVE_TARGET_UNKNOWN),
|
|
654
1057
|
`no fileUri on the save action and no ${SOURCE_URI_ARG} in the model state (clientId=${this.state.clientId})`
|
|
655
1058
|
);
|
|
656
1059
|
}
|
|
@@ -669,13 +1072,27 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
669
1072
|
* set, so re-pushing the full set clears stale markers (an empty set clears
|
|
670
1073
|
* all once the model is valid), and sharing the tool-palette validate
|
|
671
1074
|
* command's `BATCH` reason means the two paths replace rather than duplicate.
|
|
1075
|
+
*
|
|
1076
|
+
* Never rejects: the update listener that calls it is not awaited. A
|
|
1077
|
+
* validator that throws is logged as an error, markers that cannot be sent
|
|
1078
|
+
* as a warning.
|
|
672
1079
|
*/
|
|
673
1080
|
async refreshDiagnosticMarkers(): Promise<void> {
|
|
674
1081
|
if (!this.modelValidator) {
|
|
675
1082
|
return;
|
|
676
1083
|
}
|
|
677
|
-
|
|
678
|
-
|
|
1084
|
+
let markers: Marker[];
|
|
1085
|
+
try {
|
|
1086
|
+
markers = await this.modelValidator.validate([this.state.root], MarkersReason.BATCH);
|
|
1087
|
+
} catch (error: unknown) {
|
|
1088
|
+
this.logger.error(`Validation of ${this.state.sourceUri} failed: ${describeErrorWithStack(error)}`);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
this.actionDispatcher
|
|
1092
|
+
.dispatch(SetMarkersAction.create(markers, { reason: MarkersReason.BATCH }))
|
|
1093
|
+
.catch((error: unknown) =>
|
|
1094
|
+
this.logger.warn(`Could not send the markers of ${this.state.sourceUri}: ${describeErrorWithStack(error)}`)
|
|
1095
|
+
);
|
|
679
1096
|
}
|
|
680
1097
|
|
|
681
1098
|
sessionDisposed(_clientSession: ClientSession): void {
|
|
@@ -683,17 +1100,24 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
683
1100
|
}
|
|
684
1101
|
|
|
685
1102
|
/**
|
|
686
|
-
* Cancel any pending resubmit
|
|
1103
|
+
* Cancel any pending resubmit, drain every subscription — those registered
|
|
687
1104
|
* during {@link doLoadSourceModel} and the per-secondary ones, which live
|
|
688
|
-
* outside {@link toDispose} because they come and go with the write set
|
|
689
|
-
*
|
|
1105
|
+
* outside {@link toDispose} because they come and go with the write set —
|
|
1106
|
+
* and end the diagram's client session with `cause`, closing everything it
|
|
1107
|
+
* has open. Idempotent.
|
|
1108
|
+
*
|
|
1109
|
+
* The session ends last, once the detach listener is gone: its closes would
|
|
1110
|
+
* otherwise report this storage's own teardown as a client detaching.
|
|
690
1111
|
*/
|
|
691
|
-
dispose(): void {
|
|
1112
|
+
dispose(cause: SessionEndCause = 'closed'): void {
|
|
692
1113
|
this.resubmitDebouncer?.dispose();
|
|
693
1114
|
for (const subscription of this.secondarySubscriptions.values()) {
|
|
694
1115
|
subscription.dispose();
|
|
695
1116
|
}
|
|
696
1117
|
this.secondarySubscriptions.clear();
|
|
697
1118
|
this.toDispose.dispose();
|
|
1119
|
+
this.disposed = true;
|
|
1120
|
+
this.state.modelSession?.dispose(cause);
|
|
1121
|
+
this.state.modelSession = undefined;
|
|
698
1122
|
}
|
|
699
1123
|
}
|