@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.
Files changed (217) hide show
  1. package/README.md +80 -88
  2. package/lib/browser/start-glsp-server-in-worker.d.ts +43 -42
  3. package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -1
  4. package/lib/browser/start-glsp-server-in-worker.js +62 -13
  5. package/lib/browser/start-glsp-server-in-worker.js.map +1 -1
  6. package/lib/command/diagram-edit-dropped-error.d.ts +31 -0
  7. package/lib/command/diagram-edit-dropped-error.d.ts.map +1 -0
  8. package/lib/command/diagram-edit-dropped-error.js +48 -0
  9. package/lib/command/diagram-edit-dropped-error.js.map +1 -0
  10. package/lib/command/hydranium-glsp-command-stack.d.ts +33 -0
  11. package/lib/command/hydranium-glsp-command-stack.d.ts.map +1 -0
  12. package/lib/command/hydranium-glsp-command-stack.js +63 -0
  13. package/lib/command/hydranium-glsp-command-stack.js.map +1 -0
  14. package/lib/command/hydranium-glsp-operation-action-handler.d.ts +64 -0
  15. package/lib/command/hydranium-glsp-operation-action-handler.d.ts.map +1 -0
  16. package/lib/command/hydranium-glsp-operation-action-handler.js +142 -0
  17. package/lib/command/hydranium-glsp-operation-action-handler.js.map +1 -0
  18. package/lib/command/hydranium-glsp-operation-command.d.ts +250 -0
  19. package/lib/command/hydranium-glsp-operation-command.d.ts.map +1 -0
  20. package/lib/command/hydranium-glsp-operation-command.js +491 -0
  21. package/lib/command/hydranium-glsp-operation-command.js.map +1 -0
  22. package/lib/command/hydranium-glsp-recording-command.d.ts +26 -87
  23. package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -1
  24. package/lib/command/hydranium-glsp-recording-command.js +40 -117
  25. package/lib/command/hydranium-glsp-recording-command.js.map +1 -1
  26. package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts +23 -0
  27. package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts.map +1 -0
  28. package/lib/command/hydranium-glsp-undo-redo-action-handler.js +41 -0
  29. package/lib/command/hydranium-glsp-undo-redo-action-handler.js.map +1 -0
  30. package/lib/command/index.d.ts +5 -0
  31. package/lib/command/index.d.ts.map +1 -1
  32. package/lib/command/index.js +5 -0
  33. package/lib/command/index.js.map +1 -1
  34. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts +1 -1
  35. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts.map +1 -1
  36. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js +2 -2
  37. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js.map +1 -1
  38. package/lib/dispatcher/server-action-dispatcher.d.ts +25 -24
  39. package/lib/dispatcher/server-action-dispatcher.d.ts.map +1 -1
  40. package/lib/dispatcher/server-action-dispatcher.js +38 -26
  41. package/lib/dispatcher/server-action-dispatcher.js.map +1 -1
  42. package/lib/index.d.ts +1 -0
  43. package/lib/index.d.ts.map +1 -1
  44. package/lib/index.js +1 -0
  45. package/lib/index.js.map +1 -1
  46. package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +35 -59
  47. package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -1
  48. package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +60 -71
  49. package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -1
  50. package/lib/launcher/drop-notifications-to-gone-client.d.ts +23 -0
  51. package/lib/launcher/drop-notifications-to-gone-client.d.ts.map +1 -0
  52. package/lib/launcher/drop-notifications-to-gone-client.js +43 -0
  53. package/lib/launcher/drop-notifications-to-gone-client.js.map +1 -0
  54. package/lib/launcher/glsp-app-module.d.ts +1 -2
  55. package/lib/launcher/glsp-app-module.d.ts.map +1 -1
  56. package/lib/launcher/glsp-app-module.js.map +1 -1
  57. package/lib/launcher/glsp-connection-logger.d.ts +19 -0
  58. package/lib/launcher/glsp-connection-logger.d.ts.map +1 -0
  59. package/lib/launcher/glsp-connection-logger.js +24 -0
  60. package/lib/launcher/glsp-connection-logger.js.map +1 -0
  61. package/lib/launcher/glsp-server-overrides.d.ts +10 -8
  62. package/lib/launcher/glsp-server-overrides.d.ts.map +1 -1
  63. package/lib/launcher/glsp-server-overrides.js +25 -11
  64. package/lib/launcher/glsp-server-overrides.js.map +1 -1
  65. package/lib/launcher/hydranium-glsp-server.d.ts +60 -15
  66. package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -1
  67. package/lib/launcher/hydranium-glsp-server.js +145 -22
  68. package/lib/launcher/hydranium-glsp-server.js.map +1 -1
  69. package/lib/launcher/index.d.ts +2 -0
  70. package/lib/launcher/index.d.ts.map +1 -1
  71. package/lib/launcher/index.js +5 -4
  72. package/lib/launcher/index.js.map +1 -1
  73. package/lib/logging/glsp-client-logger.d.ts +10 -6
  74. package/lib/logging/glsp-client-logger.d.ts.map +1 -1
  75. package/lib/logging/glsp-client-logger.js +14 -7
  76. package/lib/logging/glsp-client-logger.js.map +1 -1
  77. package/lib/messages/index.d.ts +11 -6
  78. package/lib/messages/index.d.ts.map +1 -1
  79. package/lib/messages/index.js +11 -6
  80. package/lib/messages/index.js.map +1 -1
  81. package/lib/node/start-glsp-server.d.ts +50 -15
  82. package/lib/node/start-glsp-server.d.ts.map +1 -1
  83. package/lib/node/start-glsp-server.js +97 -30
  84. package/lib/node/start-glsp-server.js.map +1 -1
  85. package/lib/state/abstract-hydranium-glsp-state.d.ts +326 -102
  86. package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
  87. package/lib/state/abstract-hydranium-glsp-state.js +529 -139
  88. package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
  89. package/lib/state/diagram-status.d.ts +38 -0
  90. package/lib/state/diagram-status.d.ts.map +1 -0
  91. package/lib/state/diagram-status.js +27 -0
  92. package/lib/state/diagram-status.js.map +1 -0
  93. package/lib/state/full-text-hydranium-glsp-state.d.ts +22 -9
  94. package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
  95. package/lib/state/full-text-hydranium-glsp-state.js +45 -18
  96. package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
  97. package/lib/state/hydranium-glsp-index.d.ts +40 -29
  98. package/lib/state/hydranium-glsp-index.d.ts.map +1 -1
  99. package/lib/state/hydranium-glsp-index.js +82 -45
  100. package/lib/state/hydranium-glsp-index.js.map +1 -1
  101. package/lib/state/hydranium-shared-core-services.d.ts +16 -29
  102. package/lib/state/hydranium-shared-core-services.d.ts.map +1 -1
  103. package/lib/state/hydranium-shared-core-services.js +15 -28
  104. package/lib/state/hydranium-shared-core-services.js.map +1 -1
  105. package/lib/state/index.d.ts +1 -0
  106. package/lib/state/index.d.ts.map +1 -1
  107. package/lib/state/index.js +1 -0
  108. package/lib/state/index.js.map +1 -1
  109. package/lib/state/model-ready-timeout-error.d.ts.map +1 -1
  110. package/lib/state/model-ready-timeout-error.js.map +1 -1
  111. package/lib/state/reconcile-source-model-write.d.ts +13 -27
  112. package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
  113. package/lib/state/reconcile-source-model-write.js +44 -32
  114. package/lib/state/reconcile-source-model-write.js.map +1 -1
  115. package/lib/state/reconciling-multi-document-glsp-state.d.ts +125 -69
  116. package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
  117. package/lib/state/reconciling-multi-document-glsp-state.js +242 -100
  118. package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
  119. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +36 -28
  120. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
  121. package/lib/state/reconciling-transfer-hydranium-glsp-state.js +59 -42
  122. package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
  123. package/lib/status/diagram-status-reporter.d.ts +44 -0
  124. package/lib/status/diagram-status-reporter.d.ts.map +1 -0
  125. package/lib/status/diagram-status-reporter.js +88 -0
  126. package/lib/status/diagram-status-reporter.js.map +1 -0
  127. package/lib/status/hydranium-glsp-request-model-action-handler.d.ts +27 -0
  128. package/lib/status/hydranium-glsp-request-model-action-handler.d.ts.map +1 -0
  129. package/lib/status/hydranium-glsp-request-model-action-handler.js +52 -0
  130. package/lib/status/hydranium-glsp-request-model-action-handler.js.map +1 -0
  131. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts +27 -0
  132. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts.map +1 -0
  133. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js +53 -0
  134. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js.map +1 -0
  135. package/lib/status/index.d.ts +12 -0
  136. package/lib/status/index.d.ts.map +1 -0
  137. package/lib/status/index.js +12 -0
  138. package/lib/status/index.js.map +1 -0
  139. package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts +30 -0
  140. package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts.map +1 -0
  141. package/lib/storage/hydranium-glsp-request-save-model-action-handler.js +47 -0
  142. package/lib/storage/hydranium-glsp-request-save-model-action-handler.js.map +1 -0
  143. package/lib/storage/hydranium-glsp-storage.d.ts +324 -143
  144. package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
  145. package/lib/storage/hydranium-glsp-storage.js +569 -213
  146. package/lib/storage/hydranium-glsp-storage.js.map +1 -1
  147. package/lib/storage/index.d.ts +2 -1
  148. package/lib/storage/index.d.ts.map +1 -1
  149. package/lib/storage/index.js +2 -1
  150. package/lib/storage/index.js.map +1 -1
  151. package/lib/storage/save-delivery-policy.d.ts +51 -0
  152. package/lib/storage/save-delivery-policy.d.ts.map +1 -0
  153. package/lib/storage/{save-conflict-policy.js → save-delivery-policy.js} +7 -8
  154. package/lib/storage/save-delivery-policy.js.map +1 -0
  155. package/lib/submission/hydranium-glsp-submission-handler.d.ts +44 -4
  156. package/lib/submission/hydranium-glsp-submission-handler.d.ts.map +1 -1
  157. package/lib/submission/hydranium-glsp-submission-handler.js +82 -6
  158. package/lib/submission/hydranium-glsp-submission-handler.js.map +1 -1
  159. package/lib/testing/glsp-harness.d.ts +34 -7
  160. package/lib/testing/glsp-harness.d.ts.map +1 -1
  161. package/lib/testing/glsp-harness.js +47 -19
  162. package/lib/testing/glsp-harness.js.map +1 -1
  163. package/lib/validation/diagnostic-markers.d.ts +3 -3
  164. package/lib/validation/diagnostic-markers.d.ts.map +1 -1
  165. package/lib/validation/diagnostic-markers.js +1 -1
  166. package/lib/validation/diagnostic-markers.js.map +1 -1
  167. package/lib/validation/hydranium-glsp-model-validator.d.ts +1 -2
  168. package/lib/validation/hydranium-glsp-model-validator.d.ts.map +1 -1
  169. package/lib/validation/hydranium-glsp-model-validator.js +1 -1
  170. package/lib/validation/hydranium-glsp-model-validator.js.map +1 -1
  171. package/package.json +31 -52
  172. package/src/browser/start-glsp-server-in-worker.ts +79 -51
  173. package/src/command/diagram-edit-dropped-error.ts +51 -0
  174. package/src/command/hydranium-glsp-command-stack.ts +47 -0
  175. package/src/command/hydranium-glsp-operation-action-handler.ts +141 -0
  176. package/src/command/hydranium-glsp-operation-command.ts +602 -0
  177. package/src/command/hydranium-glsp-recording-command.ts +40 -121
  178. package/src/command/hydranium-glsp-undo-redo-action-handler.ts +29 -0
  179. package/src/command/index.ts +10 -0
  180. package/src/computed-bounds/hydranium-glsp-computed-bounds-action-handler.ts +2 -2
  181. package/src/dispatcher/server-action-dispatcher.ts +39 -31
  182. package/src/index.ts +1 -0
  183. package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +79 -91
  184. package/src/launcher/drop-notifications-to-gone-client.ts +47 -0
  185. package/src/launcher/glsp-app-module.ts +1 -2
  186. package/src/launcher/glsp-connection-logger.ts +27 -0
  187. package/src/launcher/glsp-server-overrides.ts +27 -11
  188. package/src/launcher/hydranium-glsp-server.ts +153 -23
  189. package/src/launcher/index.ts +5 -4
  190. package/src/logging/glsp-client-logger.ts +16 -9
  191. package/src/messages/index.ts +16 -6
  192. package/src/node/start-glsp-server.ts +133 -40
  193. package/src/state/abstract-hydranium-glsp-state.ts +609 -154
  194. package/src/state/diagram-status.ts +41 -0
  195. package/src/state/full-text-hydranium-glsp-state.ts +46 -18
  196. package/src/state/hydranium-glsp-index.ts +85 -41
  197. package/src/state/hydranium-shared-core-services.ts +16 -29
  198. package/src/state/index.ts +1 -0
  199. package/src/state/reconcile-source-model-write.ts +51 -53
  200. package/src/state/reconciling-multi-document-glsp-state.ts +276 -102
  201. package/src/state/reconciling-transfer-hydranium-glsp-state.ts +67 -45
  202. package/src/status/diagram-status-reporter.ts +106 -0
  203. package/src/status/hydranium-glsp-request-model-action-handler.ts +43 -0
  204. package/src/status/hydranium-glsp-set-edit-mode-action-handler.ts +38 -0
  205. package/src/status/index.ts +12 -0
  206. package/src/storage/hydranium-glsp-request-save-model-action-handler.ts +40 -0
  207. package/src/storage/hydranium-glsp-storage.ts +647 -223
  208. package/src/storage/index.ts +2 -1
  209. package/src/storage/save-delivery-policy.ts +49 -0
  210. package/src/submission/hydranium-glsp-submission-handler.ts +91 -5
  211. package/src/testing/glsp-harness.ts +67 -25
  212. package/src/validation/diagnostic-markers.ts +3 -3
  213. package/src/validation/hydranium-glsp-model-validator.ts +4 -5
  214. package/lib/storage/save-conflict-policy.d.ts +0 -56
  215. package/lib/storage/save-conflict-policy.d.ts.map +0 -1
  216. package/lib/storage/save-conflict-policy.js.map +0 -1
  217. package/src/storage/save-conflict-policy.ts +0 -52
@@ -16,10 +16,13 @@ var __metadata = (this && this.__metadata) || function (k, v) {
16
16
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
17
17
  };
18
18
  var AbstractHydraniumGlspState_1;
19
- import { DefaultModelState, SOURCE_URI_ARG } from '@eclipse-glsp/server';
19
+ import { DefaultModelState, EditMode, SOURCE_URI_ARG } from '@eclipse-glsp/server';
20
20
  import { Emitter } from 'vscode-jsonrpc';
21
- import { inject, injectable, optional } from 'inversify';
22
- import { DocumentState, URI } from '@hydranium/langium';
21
+ import { inject, injectable } from 'inversify';
22
+ import { AstUtils, DocumentState, isAstNode, URI } from '@hydranium/langium';
23
+ import { buildAstNode } from '@hydranium/core';
24
+ import { asModelVersion, TIMED_OUT, UNRECORDED_VERSION, describeError, describeErrorWithStack } from '@hydranium/protocol';
25
+ import { openOperationOf } from '../command/hydranium-glsp-operation-command.js';
23
26
  import { HydraniumTypes } from './hydranium-shared-core-services.js';
24
27
  import { ModelReadyTimeoutError } from './model-ready-timeout-error.js';
25
28
  /**
@@ -47,7 +50,7 @@ import { ModelReadyTimeoutError } from './model-ready-timeout-error.js';
47
50
  * override only to change the error shape (richer per-adopter output comes
48
51
  * from overriding `wsRelativePath` / `formatBuildStatus`).
49
52
  * - logging — see {@link baseTracer}. Adopters wanting a different label
50
- * override `setSourceRoot` and derive via
53
+ * override `captureSourceRoot` and derive via
51
54
  * `this.baseTracer.for('Name').withUri(uri)`.
52
55
  *
53
56
  * **Lifecycle ordering.** Reading {@link sourceUri} or {@link sourceRoot}
@@ -56,7 +59,7 @@ import { ModelReadyTimeoutError } from './model-ready-timeout-error.js';
56
59
  * (typically from the storage `loadSourceModel` flow) before reading them.
57
60
  * The {@link logger}/{@link tracer} are the exception: the injected
58
61
  * {@link baseTracer} is always available, so the getters fall back to it
59
- * before {@link setSourceRoot} derives the URI-tagged {@link _tracer} —
62
+ * before {@link captureSourceRoot} derives the URI-tagged {@link _tracer} —
60
63
  * logging is never `undefined`.
61
64
  */
62
65
  let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends DefaultModelState {
@@ -72,18 +75,71 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
72
75
  * `updateSourceModel` implementations.
73
76
  */
74
77
  conflictResolver;
78
+ /**
79
+ * Writes of one diagram edit, the first included, that may conflict before
80
+ * the edit is dropped with a warning; each further write reconciles again.
81
+ */
82
+ maxSourceModelWrites = 3;
75
83
  /**
76
84
  * Per-class tracer, caller-tagged with this state's runtime subclass name by
77
- * the `HydraniumTypes.Tracer` binding. {@link setSourceRoot} derives the
85
+ * the `HydraniumTypes.Tracer` binding. {@link captureSourceRoot} derives the
78
86
  * URI-tagged {@link _tracer} from it; the {@link tracer}/{@link logger}
79
87
  * getters fall back to it before then, so logging is never `undefined`.
80
88
  */
81
89
  baseTracer;
90
+ /**
91
+ * The client session this diagram works as, registered under `clientId` by
92
+ * the source-model storage's load and ended with the GLSP client session,
93
+ * or by a resume that takes it over. `undefined` before the load, for
94
+ * GLSP's placeholder client, and when another participant held the id, in
95
+ * which case the diagram does not load.
96
+ *
97
+ * Every write of the diagram goes through this session, which writes only a
98
+ * document it has open and opens nothing. A one-shot write to a document
99
+ * outside the write set goes through
100
+ * {@link ClientSession.withOpen}, and a document the diagram brings into
101
+ * existence through {@link ClientSession.create}.
102
+ */
103
+ modelSession;
104
+ /**
105
+ * {@link modelSession}, or a throw without one: the diagram never loaded or
106
+ * has ended. Looking `clientId` up through `ModelService.getSession` instead
107
+ * would write as whichever participant holds that id.
108
+ */
109
+ requireModelSession() {
110
+ if (!this.modelSession) {
111
+ throw new Error(`No client session is registered for ${this.clientId}; the diagram cannot write`);
112
+ }
113
+ return this.modelSession;
114
+ }
82
115
  _sourceUri;
116
+ /**
117
+ * The language {@link _sourceUri} routed to at the last capture, or
118
+ * `undefined` when it routes nowhere. Captured rather than looked up on
119
+ * each {@link diagramLanguage} read, which a GModel factory may make once
120
+ * per element, because the lookup parses the URI and walks the registry.
121
+ */
122
+ _sourceLanguage;
123
+ /**
124
+ * The root the last capture took, which every reader of the document
125
+ * shares. A projection that must see an operation's edits reads
126
+ * {@link sourceRoot}, which is a copy of it during an operation.
127
+ */
83
128
  _sourceRoot;
129
+ /** Settles when the last {@link runExclusive} call queued so far has. */
130
+ exclusiveTail = Promise.resolve();
84
131
  _tracer;
85
- _version = 0;
86
- /** Based-on versions of the secondary write set, keyed by URI. See {@link trackSecondaryDocument}. */
132
+ _baseVersion = asModelVersion(0);
133
+ /**
134
+ * The built root each secondary's version was read from, keyed by
135
+ * canonical URI, and during an operation the root of any other document it
136
+ * first reached. An operation copies a secondary from here, so the copy,
137
+ * the base a conflict reconciles from and the version its write is gated
138
+ * on describe one revision; the registry's current root can be a later
139
+ * build's.
140
+ */
141
+ capturedRoots = new Map();
142
+ /** Model versions of the secondary write set, keyed by URI. See {@link trackSecondaryDocument}. */
87
143
  _secondaryVersions = new Map();
88
144
  /** Backing emitter for {@link onSecondaryUrisChanged}; fired only on a membership change. */
89
145
  secondaryUrisChangedEmitter = new Emitter();
@@ -92,29 +148,54 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
92
148
  /** Maximum wait inside {@link readyWithTimeout} before raising {@link ModelReadyTimeoutError}. */
93
149
  readyTimeoutMs = AbstractHydraniumGlspState_1.DEFAULT_READY_TIMEOUT_MS;
94
150
  /**
95
- * Capture a freshly built source root alongside its URI, refresh the
96
- * logger to label log lines with the URI, and re-index the AST so
97
- * downstream GModel projection can resolve elements back to nodes.
151
+ * Capture a freshly built source root alongside its URI through
152
+ * {@link captureSourceRoot}, and index the AST so downstream GModel
153
+ * projection can resolve elements back to nodes.
154
+ *
155
+ * Capturing the document already captured keeps what the last GModel build
156
+ * registered in the index, which only the next build makes again, and drops
157
+ * an id added through `indexSemanticElement` whose node a rebuild replaced.
98
158
  *
99
- * Also mirrors `uri` into the inherited properties map under
100
- * {@link SOURCE_URI_ARG} so legacy GLSP code paths (`state.get(...)`)
101
- * stay consistent with the typed field.
159
+ * Throws during an operation: a capture there swaps the root out from under
160
+ * the nodes a handler resolved, and the operation captures when it ends.
102
161
  */
103
162
  setSourceRoot(uri, root) {
163
+ if (openOperationOf(this)) {
164
+ throw new Error(`setSourceRoot(${uri}) during an operation: the operation captures the source root when it ends`);
165
+ }
166
+ const recapture = uri === this._sourceUri;
167
+ this.captureSourceRoot(uri, root);
168
+ if (recapture) {
169
+ this.index.reindexSemanticElements(root, uri);
170
+ this.index.remapSemanticAliases(node => (this.isCurrentBuiltNode(node) ? node : undefined));
171
+ }
172
+ else {
173
+ this.index.indexSourceRoot(root, uri);
174
+ }
175
+ }
176
+ /**
177
+ * Take `root` as the source root and read everything derived from it: the
178
+ * base version, every tracked secondary version, the URI mirrored into the
179
+ * inherited properties map under {@link SOURCE_URI_ARG}, and the
180
+ * URI-labelled logger. Everything but the index, which
181
+ * {@link setSourceRoot} sets. Override to derive more from a capture.
182
+ */
183
+ captureSourceRoot(uri, root) {
104
184
  this._sourceUri = uri;
185
+ this._sourceLanguage = this.sharedServices.ServiceRegistry.getServicesFor(uri);
105
186
  this._sourceRoot = root;
106
- this._version = this.readDocumentVersion(uri);
107
- // Re-read the secondary write set for the same reason the primary is
108
- // re-read: the write that brought us here advanced their versions too, and
109
- // a stale based-on version would gate the NEXT command against a
110
- // superseded number — reporting a conflict that is really this state's own
111
- // last write.
187
+ // The root's own version: the registry's current root can be a later
188
+ // build's, and a write based on its version passes the gate over edits
189
+ // `root` never saw.
190
+ this._baseVersion = this.sharedServices.workspace.ModelLedger.versionOf(root);
191
+ // Re-read the secondary write set: the write that brought us here
192
+ // advanced their versions too, and a stale base version would gate the
193
+ // NEXT command against a superseded number — reporting a conflict that is
194
+ // really this state's own last write.
112
195
  this.refreshSecondaryVersions();
113
196
  this.set(SOURCE_URI_ARG, uri);
114
197
  this._tracer = this.baseTracer.withUri(uri);
115
- this.tracer.debug(`Captured source root at doc.version=v${this._version}`);
116
- this.checkDeclaredLanguage(uri);
117
- this.index.indexSourceRoot(root, uri);
198
+ this.tracer.debug(`Captured source root at doc.version=v${this._baseVersion}`);
118
199
  }
119
200
  /**
120
201
  * Typed override of {@link DefaultModelState.sourceUri}. Narrows the
@@ -124,32 +205,226 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
124
205
  get sourceUri() {
125
206
  return this._sourceUri;
126
207
  }
208
+ /** The root operation handlers edit: during an operation a copy of the built root, outside one the built root. */
127
209
  get sourceRoot() {
128
- return this._sourceRoot;
210
+ return openOperationOf(this) && this._sourceRoot !== undefined ? this.workingRootOf(this._sourceUri) : this._sourceRoot;
211
+ }
212
+ /**
213
+ * Run `run` once every earlier call has settled, thrown or not: the
214
+ * boundary that orders operations, undo and redo, and the storage's capture
215
+ * and render. Code running inside it never calls this again, since the
216
+ * call would wait for the section it runs in.
217
+ */
218
+ runExclusive(run) {
219
+ const result = this.exclusiveTail.then(() => run());
220
+ this.exclusiveTail = result.catch(() => undefined);
221
+ return result;
222
+ }
223
+ /**
224
+ * The root of `uri` that operation handlers edit: during an operation a
225
+ * copy made on the first request and returned for the rest of it, outside
226
+ * one the root itself. The root is the built root of `uri`, or `orElse()`
227
+ * when `uri` has no document; `undefined` when neither gives one.
228
+ *
229
+ * A handler that edits another document than the source reaches its root
230
+ * through this, or the edit lands on the root every reader shares. An id
231
+ * added through `indexSemanticElement` for a node of that root names the
232
+ * node's copy from then on.
233
+ */
234
+ workingRootOf(uri, orElse) {
235
+ const key = this.workingKey(uri);
236
+ const primary = this._sourceUri !== undefined && key === this.workingKey(this._sourceUri);
237
+ const operation = openOperationOf(this);
238
+ if (!operation) {
239
+ return primary ? this._sourceRoot : (this.sharedServices.model.ModelService.getDocument(uri)?.parseResult.value ?? orElse?.());
240
+ }
241
+ let built = primary ? this._sourceRoot : this.capturedRoots.get(key);
242
+ if (built === undefined) {
243
+ // First reached during the operation: frozen now, for the rest of it.
244
+ built = this.readBuiltRoot(uri);
245
+ if (built !== undefined) {
246
+ this.capturedRoots.set(key, built);
247
+ }
248
+ }
249
+ built ??= orElse?.();
250
+ return built === undefined ? undefined : operation.workingRootOf(key, built);
251
+ }
252
+ /** The copy of `uri` the open operation made, without making one. */
253
+ existingWorkingRootOf(uri) {
254
+ return openOperationOf(this)?.existingWorkingRootOf(this.workingKey(uri));
129
255
  }
130
256
  /**
131
- * The language services of the grammar this DIAGRAM TYPE edits, as declared
132
- * by `AbstractHydraniumGlspDiagramModule.declareLanguage`.
257
+ * The key of `uri` among an operation's copies: its canonical form, so two
258
+ * spellings of one document share one copy.
259
+ */
260
+ workingKey(uri) {
261
+ return this.sharedServices.workspace.DocumentUriPolicy.canonicalUri(uri);
262
+ }
263
+ /**
264
+ * The built node `node` was copied from during an operation; `node` itself
265
+ * for a built node, for a node the operation created, and outside one.
266
+ *
267
+ * A copy has no `$document`, so a lookup that reads its document, scope or
268
+ * project throws or answers for no document. Handlers pass the built node
269
+ * there and keep editing the copy. A candidate query takes
270
+ * {@link referenceInfoOf} instead, since a created node has no built node.
271
+ */
272
+ builtNodeOf(node) {
273
+ return openOperationOf(this)?.builtNodeOf(node) ?? node;
274
+ }
275
+ /**
276
+ * A reference to `target` whose `ref` is `target` itself, a copy node
277
+ * included, so an identity comparison later in the operation finds it.
278
+ * `undefined` when no reference can be formed. Outside an operation the
279
+ * nodes are built ones and this is the `ReferenceBuilder`'s answer as is.
280
+ *
281
+ * With `tier: 'own'` the `$refText` is the target's own name as
282
+ * `toOwnReference` gives it, read off `target` itself, so a name the
283
+ * operation edited is the one written; the builder is the target's
284
+ * language's, which a copy node, having no `$document`, would not otherwise
285
+ * reach. `source` plays no part in that tier.
133
286
  *
134
- * **Named for what it is, not for "the language".** Reach for it only when
135
- * the thing you are asking about lives in the diagram document itself.
136
- * Anything reached *through* a reference is decided by its own document: use
137
- * {@link languageServicesFor}. Making that choice explicit at the call site
138
- * is the point; a neutral-looking `languageServices` invites the diagram's
139
- * grammar to be applied to a foreign node, which is the defect this whole
140
- * seam exists to prevent.
287
+ * By default it is `getReferenceName` from `source`'s context, with the
288
+ * builder of `source`'s language, since `$refText` encoding follows the
289
+ * grammar writing it. That call reads both nodes' projects, to pick the
290
+ * qualification and to refuse a reference across projects the target's is
291
+ * not visible to, and a copy node has none; so it is asked about the built
292
+ * target, which names a target as built, before any rename in the
293
+ * operation, and about {@link builtContextOf} the source. A target the
294
+ * operation created has no built node, and is asked about without a
295
+ * project.
296
+ */
297
+ referenceTo(target, source, options = {}) {
298
+ const refText = options.tier === 'own'
299
+ ? this.languageServicesFor(target)?.references.ReferenceBuilder.toOwnReference(target)?.$refText
300
+ : this.languageServicesFor(source ?? target)?.references.ReferenceBuilder.getReferenceName(this.builtNodeOf(target), source === undefined ? undefined : this.builtContextOf(source));
301
+ return refText === undefined ? undefined : { ref: target, $refText: refText };
302
+ }
303
+ /**
304
+ * A candidate query for `property` of `node`, to pass to
305
+ * `candidateProviderFor(node)?.find`. It is asked from the built node, and
306
+ * for a node the operation created from a stand-in of its type under the
307
+ * built containers it sits in, one per created level: a scope reads local
308
+ * names off every container up the chain, so anchoring higher drops them.
309
+ * A protocol context cannot express this: an element source resolves only
310
+ * through a `resolveElementByName` override, and through the index only
311
+ * for the nodes it exports. `undefined` for a node whose `$container` chain
312
+ * reaches no document and no copy, such as a node built with no
313
+ * `$container` that is not a document's root.
141
314
  *
142
- * Injected rather than derived from {@link sourceUri} so it is available
143
- * before the first {@link setSourceRoot} — GLSP constructs operation
144
- * handlers at `InitializeClientSession`, well before `RequestModelAction`.
145
- * `@optional()` so a harness that binds no diagram module still resolves;
146
- * `undefined` then means no diagram module declared a grammar.
315
+ * A node not created yet is asked about through a stand-in built with
316
+ * `buildAstNode`: its type, `$container` the container it would join, and
317
+ * `$containerProperty`, left unattached.
318
+ */
319
+ referenceInfoOf(node, property) {
320
+ let container = this.builtContextOf(node);
321
+ if (!AstUtils.findRootNode(container).$document) {
322
+ return undefined;
323
+ }
324
+ const created = [];
325
+ for (let level = node; level && this.builtNodeOf(level) !== container; level = level.$container) {
326
+ created.unshift(level);
327
+ }
328
+ for (const level of created) {
329
+ container = buildAstNode(this.sharedServices.AstReflection, level.$type, {
330
+ $container: container,
331
+ $containerProperty: level.$containerProperty,
332
+ $containerIndex: level.$containerIndex
333
+ });
334
+ }
335
+ return { reference: { $refText: '', ref: undefined }, container, property };
336
+ }
337
+ /**
338
+ * The built node of `node`, or of its nearest container that has one: a
339
+ * node the operation created has no built node, but its container's
340
+ * document and project are its own.
341
+ */
342
+ builtContextOf(node) {
343
+ const operation = openOperationOf(this);
344
+ for (let current = node; current; current = current.$container) {
345
+ const built = this.builtNodeOf(current);
346
+ if (built !== current || operation?.workingUriOf(current) === undefined) {
347
+ return built;
348
+ }
349
+ }
350
+ return node;
351
+ }
352
+ /**
353
+ * Take `root`, which a write of this state's own produced, as the source
354
+ * root; during an operation the operation takes it and captures it when it
355
+ * ends.
356
+ */
357
+ captureWrittenRoot(root) {
358
+ const operation = openOperationOf(this);
359
+ if (operation) {
360
+ operation.recordWrittenRoot(root);
361
+ }
362
+ else {
363
+ this.setSourceRoot(this._sourceUri, root);
364
+ }
365
+ }
366
+ /**
367
+ * Resync after a write whose reconcile dropped the edit: during an
368
+ * operation the operation undoes its side effects, pushes nothing, and
369
+ * captures the document's current root when it ends.
147
370
  */
148
- diagramLanguage;
371
+ writeDropped() {
372
+ const operation = openOperationOf(this);
373
+ if (operation) {
374
+ operation.recordDropped(this.sharedServices.model.ModelService.getDocument(this._sourceUri)?.parseResult.value);
375
+ }
376
+ else {
377
+ this.refreshSourceRoot();
378
+ }
379
+ }
380
+ /**
381
+ * Take the document's built root as the source root; during an operation,
382
+ * as the root the operation closes on. A write the document already held,
383
+ * and an undo or redo that wrote nothing while its steps may have rebuilt
384
+ * the document, leave this state on a root that predates the document. The
385
+ * built root can lag the text until the next build, whose refresh captures
386
+ * that, as after {@link writeDropped}.
387
+ */
388
+ captureBuiltRoot() {
389
+ const operation = openOperationOf(this);
390
+ if (!operation) {
391
+ this.refreshSourceRoot();
392
+ return;
393
+ }
394
+ const current = this.sharedServices.model.ModelService.getDocument(this._sourceUri)?.parseResult.value;
395
+ if (current !== undefined) {
396
+ operation.recordWrittenRoot(current);
397
+ }
398
+ }
399
+ /**
400
+ * Whether `node` belongs to the root its document is built to now, or to no
401
+ * document; a root a rebuild replaced holds only stale nodes.
402
+ */
403
+ isCurrentBuiltNode(node) {
404
+ const root = AstUtils.findRootNode(node);
405
+ const document = root.$document;
406
+ return (document === undefined || this.sharedServices.model.ModelService.getDocument(document.uri.toString())?.parseResult.value === root);
407
+ }
408
+ /**
409
+ * The language services of the diagram document: the language
410
+ * {@link sourceUri} routed to at the last {@link setSourceRoot}.
411
+ *
412
+ * `undefined` until a document is loaded, as GLSP builds a session's
413
+ * handlers at `InitializeClientSession`, before `RequestModelAction`
414
+ * supplies the URI: read it when called, not at construction.
415
+ *
416
+ * Reach for it only when the thing you are asking about lives in the
417
+ * diagram document itself. Anything reached through a reference is decided
418
+ * by its own document: use {@link languageServicesFor}.
419
+ */
420
+ get diagramLanguage() {
421
+ return this._sourceLanguage;
422
+ }
149
423
  /**
150
424
  * The language services owning `target` — an AST node (routed by its
151
- * document), a URI, or a URI string — falling back to
152
- * {@link diagramLanguage} when `target` routes nowhere.
425
+ * document), a URI, or a URI string — falling back, for a node, to the
426
+ * language the index read from the diagram's root, and otherwise to
427
+ * {@link diagramLanguage}, when `target` routes nowhere.
153
428
  *
154
429
  * Use this wherever the thing being named, keyed or completed may live in
155
430
  * another document than the diagram. Two registered languages are free to
@@ -159,8 +434,14 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
159
434
  * diagnostic. The fallback covers the genuinely unroutable case, a synthetic
160
435
  * node not yet attached to a document, where the diagram's own language is
161
436
  * the only defensible answer.
437
+ *
438
+ * A node routes through {@link HydraniumGlspIndex.languageServicesFor}, so
439
+ * an override of the index's routing applies here too.
162
440
  */
163
441
  languageServicesFor(target) {
442
+ if (isAstNode(target)) {
443
+ return this.index.languageServicesFor(target) ?? this.diagramLanguage;
444
+ }
164
445
  return this.sharedServices.ServiceRegistry.getServicesFor(target) ?? this.diagramLanguage;
165
446
  }
166
447
  /**
@@ -186,64 +467,46 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
186
467
  candidateProviderFor(target) {
187
468
  return this.languageServicesFor(target)?.references.CandidateProvider;
188
469
  }
189
- /**
190
- * Warn when the loaded document does not route to the grammar this diagram
191
- * module declared.
192
- *
193
- * The declared language is the one fact the module states that the grammar
194
- * itself does not, so it is the one that can drift — a diagram type pointed
195
- * at the wrong `LanguageMetaData`, or a file extension reassigned. Both
196
- * would otherwise surface much later as references resolving against the
197
- * wrong scope. Only a warning, not a throw: the document is loaded and
198
- * usable either way, and a hard failure here would take out a diagram over
199
- * a mismatch that may be intentional in an adopter serving one diagram type
200
- * over several grammars.
201
- */
202
- checkDeclaredLanguage(uri) {
203
- const declared = this.diagramLanguage?.LanguageMetaData.languageId;
204
- if (declared === undefined) {
205
- return;
206
- }
207
- const actual = this.sharedServices.ServiceRegistry.getServicesFor(uri)?.LanguageMetaData.languageId;
208
- if (actual !== undefined && actual !== declared) {
209
- this.tracer.warn(`Diagram module declares language '${declared}' but this document routes to '${actual}'`);
210
- }
211
- }
212
470
  /** The observability handle as a plain {@link Logger} (a {@link Tracer} is-a Logger). */
213
471
  get logger() {
214
472
  return this.tracer;
215
473
  }
216
- /** URI-tagged once {@link setSourceRoot} has run; the class-tagged injected {@link baseTracer} before that. */
474
+ /** URI-tagged once a source root is captured; the class-tagged injected {@link baseTracer} before that. */
217
475
  get tracer() {
218
476
  return this._tracer ?? this.baseTracer;
219
477
  }
220
478
  /**
221
- * Text-document version captured at the last {@link setSourceRoot}
222
- * call. Mirrors the server-side `TextDocuments.version(uri)` counter
223
- * at that point in time — frozen until the next `setSourceRoot`, then
224
- * re-read from `LangiumDocuments`.
479
+ * Model version of the root the last {@link captureSourceRoot} took,
480
+ * frozen until the next one.
225
481
  *
226
- * Threaded by `HydraniumGlspRecordingCommand` into
227
- * {@link updateSourceModel}'s optional `version` parameter so the
228
- * downstream `ModelService.update` / `.save` call can opt into the
229
- * conflict gate against the captured based-on version. See
482
+ * The base an operation's write is gated on, taken when the operation
483
+ * opens; nothing captures during an operation, so it is the version of the
484
+ * root the operation's copies were made from. See
230
485
  * `@hydranium/protocol#ConflictError` for the detection contract.
231
486
  *
232
- * Falls back to `0` when the document is absent from `LangiumDocuments`
233
- * at capture time — the same observable state callers see for a doc
234
- * that was never opened. Callers that need to distinguish
235
- * "never-versioned" from "v0" must consult the document registry
236
- * directly.
487
+ * **This, not a number read at write time, is what a write must be gated
488
+ * on.** A version read when the write is issued is the store's current one,
489
+ * so it matches by construction and the gate can never fire.
490
+ */
491
+ get baseVersion() {
492
+ return this._baseVersion;
493
+ }
494
+ /**
495
+ * {@link baseVersion} as a plain number: the store's version of the text the
496
+ * root was parsed from.
497
+ *
498
+ * `UNRECORDED_VERSION` when the document was not built when the snapshot
499
+ * was taken, which no write matches.
237
500
  */
238
501
  get version() {
239
- return this._version;
502
+ return this._baseVersion;
240
503
  }
241
504
  // ============================================================
242
505
  // Secondary documents — the write set beyond the primary
243
506
  // ============================================================
244
507
  /**
245
- * Register `uri` as a document this state also writes, capturing its current
246
- * text-document version.
508
+ * Register `uri` as a document this state also writes, snapshotting its
509
+ * current text-document version.
247
510
  *
248
511
  * A diagram whose layout lives in its own file, or whose canvas creates
249
512
  * elements in a referenced semantic document, writes more than the document
@@ -256,7 +519,7 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
256
519
  * identified once the operation names the node. What the framework supplies
257
520
  * is the part an adopter cannot reconstruct after the fact — the version each
258
521
  * document was at BEFORE the command mutated anything (see
259
- * {@link capturedVersionOf}). Idempotent; registering the primary is ignored,
522
+ * {@link baseVersionOf}). Idempotent; registering the primary is ignored,
260
523
  * since {@link version} already tracks it. Registering a URI not already in
261
524
  * the set fires {@link onSecondaryUrisChanged}.
262
525
  */
@@ -265,7 +528,10 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
265
528
  return;
266
529
  }
267
530
  const added = !this._secondaryVersions.has(uri);
268
- this._secondaryVersions.set(uri, this.readDocumentVersion(uri));
531
+ const key = this.workingKey(uri);
532
+ // During an operation, the root the operation copied or first reached, not a later build's.
533
+ const root = (openOperationOf(this) ? this.capturedRoots.get(key) : undefined) ?? this.readBuiltRoot(uri);
534
+ this.recordSecondary(uri, root);
269
535
  if (added) {
270
536
  this.secondaryUrisChangedEmitter.fire();
271
537
  }
@@ -283,14 +549,15 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
283
549
  }
284
550
  /**
285
551
  * Fires whenever the secondary write set gains or loses a URI — never when a
286
- * tracked document's captured version is merely refreshed, which happens on
287
- * every {@link setSourceRoot} and changes nothing a subscriber cares about.
552
+ * tracked document's model version is merely refreshed, which happens on
553
+ * every {@link captureSourceRoot} and changes nothing a subscriber cares about.
288
554
  *
289
555
  * **This exists so no caller has to know when the set can change.** The set is
290
556
  * usually discovered rather than known, and the discovery point is an
291
- * operation handler naming a node — not a capture. A subscriber that instead
292
- * re-derived the set after each capture would miss exactly that case, and miss
293
- * it silently: the document would simply never be watched, with nothing logged.
557
+ * operation handler naming a node — not a read of the source root. A
558
+ * subscriber that instead re-derived the set after each such read would miss
559
+ * exactly that case, and miss it silently: the document would simply never be
560
+ * watched, with nothing logged.
294
561
  * Announcing the change here moves the obligation to the one place that can
295
562
  * always discharge it.
296
563
  *
@@ -306,37 +573,104 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
306
573
  get secondaryUris() {
307
574
  return [...this._secondaryVersions.keys()];
308
575
  }
576
+ /** Active statuses, least recently set first. See {@link setStatus}. */
577
+ statuses = new Map();
578
+ /** Backing emitter for {@link onStatusChanged}. */
579
+ statusChangedEmitter = new Emitter();
309
580
  /**
310
- * Based-on version captured for `uri` — {@link version} for the primary, the
311
- * value captured at {@link trackSecondaryDocument} (refreshed by
312
- * {@link setSourceRoot}) for a secondary, `undefined` for anything untracked.
581
+ * Set `status`'s entry, or withdraw it with `undefined`, and recompute
582
+ * {@link currentStatus} and {@link editMode} from every active entry.
313
583
  *
314
- * `undefined` rather than `0` for an untracked URI deliberately: `0` is a real
584
+ * `editMode` is derived here and nowhere else: a writer that assigns it
585
+ * directly is overwritten by the next status change, and its write is not
586
+ * announced on {@link onStatusChanged}.
587
+ */
588
+ setStatus(status, entry) {
589
+ const previousEntry = this.statuses.get(status);
590
+ if (previousEntry === undefined && entry === undefined) {
591
+ return;
592
+ }
593
+ const previousStatus = this.currentStatus;
594
+ const previousEditMode = this.editMode;
595
+ // Re-inserted so iteration order is recency, which breaks a severity tie.
596
+ this.statuses.delete(status);
597
+ if (entry !== undefined) {
598
+ this.statuses.set(status, entry);
599
+ }
600
+ this.editMode = [...this.statuses.values()].some(active => active.readonly) ? EditMode.READONLY : EditMode.EDITABLE;
601
+ this.logger.debug(`Status ${status}: ${describeStatusEntry(previousEntry)} → ${describeStatusEntry(entry)}`);
602
+ if (this.currentStatus !== previousStatus || this.editMode !== previousEditMode) {
603
+ this.statusChangedEmitter.fire();
604
+ }
605
+ }
606
+ /** The active entry whose message the client shows: highest severity, then most recently set. */
607
+ get currentStatus() {
608
+ let winner;
609
+ for (const entry of this.statuses.values()) {
610
+ if (entry.message !== undefined && (!winner || severityRank(entry) >= severityRank(winner))) {
611
+ winner = entry;
612
+ }
613
+ }
614
+ return winner;
615
+ }
616
+ /** The active statuses that block editing; empty while the diagram is editable. */
617
+ get readonlyStatuses() {
618
+ return [...this.statuses].filter(([, entry]) => entry.readonly).map(([status]) => status);
619
+ }
620
+ /** The status that supplies {@link currentStatus}, for a log line naming it. */
621
+ get currentStatusSource() {
622
+ const current = this.currentStatus;
623
+ return [...this.statuses].find(([, entry]) => entry === current)?.[0];
624
+ }
625
+ /** Fires when {@link currentStatus} or {@link editMode} changes. */
626
+ get onStatusChanged() {
627
+ return this.statusChangedEmitter.event;
628
+ }
629
+ /**
630
+ * The base version of a write of `uri`: {@link baseVersion} for the primary,
631
+ * the version taken at {@link trackSecondaryDocument} (refreshed by
632
+ * {@link captureSourceRoot}) for a secondary, `undefined` for anything untracked.
633
+ *
634
+ * `undefined` rather than v0 for an untracked URI deliberately: `0` is a real
315
635
  * version meaning "present but never edited", so collapsing the two would let
316
- * a caller gate a write against a document it never captured.
636
+ * a caller gate a write against a document this state never read.
317
637
  */
318
- capturedVersionOf(uri) {
638
+ baseVersionOf(uri) {
319
639
  if (uri === this._sourceUri) {
320
- return this._version;
640
+ return this._baseVersion;
321
641
  }
322
642
  return this._secondaryVersions.get(uri);
323
643
  }
644
+ /** Re-take every registered secondary's built root and the model version of that root. */
645
+ refreshSecondaryVersions() {
646
+ this.capturedRoots.clear();
647
+ for (const uri of [...this._secondaryVersions.keys()]) {
648
+ this.recordSecondary(uri, this.readBuiltRoot(uri));
649
+ }
650
+ }
324
651
  /**
325
- * Read a document's current text-document version through the model service's
326
- * canonicalizing gateway, so a URI spelled through a symlink still resolves to
327
- * the document keyed by its real path rather than stranding at `0` (which
328
- * would silently weaken the conflict gate). Optional-chained on `textDocument`
329
- * so a fixture that omits it does not throw; real Langium documents always
330
- * populate it.
652
+ * The built root of `uri`, looked up through the model service's
653
+ * canonicalizing gateway so a symlinked URI does not strand without one;
654
+ * `undefined` for a URI with no parsed document, whose version is then
655
+ * `UNRECORDED_VERSION`, which no write matches. The store counts a file it
656
+ * has not built at `0`, and so does the placeholder the builder registers
657
+ * before its parse, and a write based on `0` overwrites text this state
658
+ * never read.
331
659
  */
332
- readDocumentVersion(uri) {
333
- return this.sharedServices.model.ModelService.getDocument(uri)?.textDocument?.version ?? 0;
660
+ readBuiltRoot(uri) {
661
+ const root = this.sharedServices.model.ModelService.getDocument(uri)?.parseResult.value;
662
+ return root && !this.sharedServices.workspace.ModelLedger.isPlaceholder(root) ? root : undefined;
334
663
  }
335
- /** Re-read every registered secondary's version from the document store. */
336
- refreshSecondaryVersions() {
337
- for (const uri of [...this._secondaryVersions.keys()]) {
338
- this._secondaryVersions.set(uri, this.readDocumentVersion(uri));
664
+ /** Take `root` as the secondary `uri` was read at, and its version, `UNRECORDED_VERSION` without one. */
665
+ recordSecondary(uri, root) {
666
+ if (root !== undefined) {
667
+ this.capturedRoots.set(this.workingKey(uri), root);
339
668
  }
669
+ this._secondaryVersions.set(uri, root ? this.sharedServices.workspace.ModelLedger.versionOf(root) : UNRECORDED_VERSION);
670
+ }
671
+ /** The root the secondary `uri` was read at; see {@link capturedRoots}. */
672
+ capturedRootOf(uri) {
673
+ return this.capturedRoots.get(this.workingKey(uri));
340
674
  }
341
675
  /**
342
676
  * Re-capture the source root from the current Langium document,
@@ -346,7 +680,8 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
346
680
  * handlers) must see the root the freshly built document carries.
347
681
  */
348
682
  refreshSourceRoot() {
349
- if (!this._sourceUri) {
683
+ // The operation captures when it ends.
684
+ if (!this._sourceUri || openOperationOf(this)) {
350
685
  return;
351
686
  }
352
687
  const document = this.sharedServices.model.ModelService.getDocument(this._sourceUri);
@@ -359,48 +694,81 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
359
694
  this.setSourceRoot(this._sourceUri, root);
360
695
  }
361
696
  }
697
+ /**
698
+ * The root that `uri`'s text parses to (the text store's, or the built
699
+ * document's when the store holds none) and the store's version of that
700
+ * text, read once the document has validated; `undefined` when it cannot be
701
+ * read. The version is read in the tick the text is, so a write based on it
702
+ * conflicts with any edit made after.
703
+ *
704
+ * **Parsed afresh, never the built root.** The built root can lag the
705
+ * store, and a root behind the text it is versioned with makes the replay
706
+ * drop the edits it lacks. The root is parsed, not linked: an encoder hook
707
+ * reading a reference's `ref` sees `undefined` on it.
708
+ */
709
+ async readCurrentRoot(uri) {
710
+ const modelService = this.sharedServices.model.ModelService;
711
+ // Unreadable here is the conflict the caller then raises, which is logged where it ends; the reason is diagnosis.
712
+ const validated = await modelService.validated(uri).catch((error) => {
713
+ this.logger.debug(`The current text of ${uri} cannot be read: ${describeError(error)}`);
714
+ return undefined;
715
+ });
716
+ if (!validated) {
717
+ return undefined;
718
+ }
719
+ const store = this.sharedServices.workspace.TextDocuments;
720
+ const text = store.get(uri)?.getText() ?? modelService.getDocument(uri)?.textDocument.getText();
721
+ if (text === undefined) {
722
+ return undefined;
723
+ }
724
+ const version = asModelVersion(store.version(uri));
725
+ return { root: this.sharedServices.workspace.LangiumDocumentFactory.fromString(text, URI.parse(uri)).parseResult.value, version };
726
+ }
362
727
  /**
363
728
  * Wait until the captured document reaches `state`, then run
364
729
  * {@link onReadyRefreshed} so consumers see a fresh AST. Wraps the
365
730
  * wait in {@link Tracer.time} for observability.
731
+ *
732
+ * A source not in the document registry is built first, through
733
+ * `ModelService.rebuild`. For a source with neither a file nor text, that
734
+ * build leaves no document, and this rejects rather than timing out.
366
735
  */
367
736
  async ready(state) {
368
737
  await this.tracer.time(`Wait for state '${DocumentState[state]}'`, () => this.readyWithTimeout(state), 'debug');
369
738
  await this.onReadyRefreshed();
370
739
  }
371
740
  /**
372
- * Race `ModelService.waitForDocumentState` against
373
- * {@link readyTimeoutMs}. If the timeout fires but the document has
374
- * already reached `state` silently (Langium build-phase event race),
375
- * log a warning and resolve; otherwise raise
376
- * {@link ModelReadyTimeoutError} via {@link buildReadyTimeoutError}.
741
+ * Race `ModelService.ensureDocumentState`, which builds a root behind its
742
+ * text, against {@link readyTimeoutMs}. If the timeout fires but the
743
+ * document has already reached `state`, log a warning naming the cause, a
744
+ * root behind its text or a missed phase event, and resolve; otherwise
745
+ * raise {@link ModelReadyTimeoutError} via {@link buildReadyTimeoutError}.
377
746
  */
378
747
  async readyWithTimeout(state) {
379
748
  const uri = this._sourceUri;
380
749
  const stopwatch = this.sharedServices.Clock.stopwatch();
381
- let timer;
382
- const timeout = new Promise((resolve, reject) => {
383
- timer = this.sharedServices.Clock.setTimer(() => {
384
- const elapsed = Math.round(stopwatch.elapsedMs);
385
- // Via the canonicalizing gateway (see `readDocumentVersion`), so a
386
- // symlinked `_sourceUri` is not reported as a hard timeout.
387
- const doc = this.sharedServices.model.ModelService.getDocument(uri);
388
- if (doc && doc.state >= state) {
389
- this.logger.warn(`Missed '${DocumentState[state]}' notification after ${elapsed}ms; document is already at state ` +
390
- `'${DocumentState[doc.state]}'. Likely a Langium build-phase event race.`);
391
- resolve();
392
- }
393
- else {
394
- reject(this.buildReadyTimeoutError(state, elapsed));
395
- }
396
- }, this.readyTimeoutMs);
397
- });
398
- try {
399
- await Promise.race([this.sharedServices.model.ModelService.waitForDocumentState(uri, state), timeout]);
750
+ const reached = this.sharedServices.model.ModelService.ensureDocumentState(uri, state);
751
+ if ((await this.sharedServices.Clock.raceTimer(reached, this.readyTimeoutMs)) !== TIMED_OUT) {
752
+ return;
400
753
  }
401
- finally {
402
- timer?.dispose();
754
+ // The race has settled, so a later failure of the wait, such as a given-up sync build, reaches no one else.
755
+ void reached.catch((error) => this.logger.error(`The wait for '${DocumentState[state]}' failed after it timed out: ${describeErrorWithStack(error)}`));
756
+ const elapsed = Math.round(stopwatch.elapsedMs);
757
+ // Through the model service's canonicalizing gateway, so a symlinked
758
+ // `_sourceUri` is not reported as a hard timeout.
759
+ const doc = this.sharedServices.model.ModelService.getDocument(uri);
760
+ if (!doc || doc.state < state) {
761
+ throw this.buildReadyTimeoutError(state, elapsed);
762
+ }
763
+ const built = this.sharedServices.workspace.ModelLedger.versionOf(doc.parseResult.value);
764
+ const text = this.sharedServices.workspace.TextDocuments.version(uri);
765
+ if (built !== UNRECORDED_VERSION && built < text) {
766
+ this.logger.warn(`No build caught the root up after ${elapsed}ms: the document is at state '${DocumentState[doc.state]}', ` +
767
+ `but its root was parsed from v${built} and the text is at v${text}.`);
768
+ return;
403
769
  }
770
+ this.logger.warn(`Missed '${DocumentState[state]}' notification after ${elapsed}ms; document is already at state ` +
771
+ `'${DocumentState[doc.state]}'. Likely a Langium build-phase event race.`);
404
772
  }
405
773
  /**
406
774
  * Hook for post-{@link ready} work. Default refreshes the captured
@@ -426,6 +794,20 @@ let AbstractHydraniumGlspState = class AbstractHydraniumGlspState extends Defaul
426
794
  const status = workspace.DocumentBuilder.formatBuildStatus(URI.parse(this._sourceUri));
427
795
  return new ModelReadyTimeoutError(shortPath, DocumentState[state], elapsed, status);
428
796
  }
797
+ /**
798
+ * `transition` as the documents would hold both ends after a write. A
799
+ * serializer may normalize what it writes, provided normalizing again
800
+ * changes nothing; an end left as recorded makes an undo or redo conflict
801
+ * with the write it follows. Default: unchanged.
802
+ */
803
+ async normalizeTransition(transition) {
804
+ return transition;
805
+ }
806
+ /** The root `model` parses back to once serialized for `uri`, as a write would leave it; nothing is written. */
807
+ async roundTrip(uri, model) {
808
+ const text = await this.sharedServices.model.ModelService.modelToText(uri, model);
809
+ return this.sharedServices.workspace.LangiumDocumentFactory.fromString(text, URI.parse(uri)).parseResult.value;
810
+ }
429
811
  };
430
812
  __decorate([
431
813
  inject(HydraniumTypes.SharedCoreServices),
@@ -439,13 +821,21 @@ __decorate([
439
821
  inject(HydraniumTypes.Tracer),
440
822
  __metadata("design:type", Object)
441
823
  ], AbstractHydraniumGlspState.prototype, "baseTracer", void 0);
442
- __decorate([
443
- inject(HydraniumTypes.DiagramLanguage),
444
- optional(),
445
- __metadata("design:type", Object)
446
- ], AbstractHydraniumGlspState.prototype, "diagramLanguage", void 0);
447
824
  AbstractHydraniumGlspState = AbstractHydraniumGlspState_1 = __decorate([
448
825
  injectable()
449
826
  ], AbstractHydraniumGlspState);
450
827
  export { AbstractHydraniumGlspState };
828
+ const SEVERITY_RANK = { INFO: 0, WARNING: 1, ERROR: 2, FATAL: 3 };
829
+ /** A message without a severity ranks as `INFO`. */
830
+ function severityRank(entry) {
831
+ return SEVERITY_RANK[entry.severity ?? 'INFO'];
832
+ }
833
+ function describeStatusEntry(entry) {
834
+ if (entry === undefined) {
835
+ return 'inactive';
836
+ }
837
+ const flags = [entry.message === undefined ? undefined : (entry.severity ?? 'INFO'), entry.readonly ? 'readonly' : undefined];
838
+ const message = entry.message === undefined ? '' : ` "${entry.message}"`;
839
+ return `${flags.filter(flag => flag !== undefined).join(' ') || 'active'}${message}`;
840
+ }
451
841
  //# sourceMappingURL=abstract-hydranium-glsp-state.js.map