@hydranium/core 1.0.0-next.8 → 1.0.0-next.85

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 (306) hide show
  1. package/lib/documents/ast-document-manager.d.ts +75 -16
  2. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  3. package/lib/documents/ast-document-manager.js +57 -19
  4. package/lib/documents/ast-document-manager.js.map +1 -1
  5. package/lib/documents/client-ids.d.ts +5 -19
  6. package/lib/documents/client-ids.d.ts.map +1 -1
  7. package/lib/documents/client-ids.js +5 -19
  8. package/lib/documents/client-ids.js.map +1 -1
  9. package/lib/documents/hydranium-text-documents.d.ts +55 -21
  10. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  11. package/lib/documents/hydranium-text-documents.js +142 -46
  12. package/lib/documents/hydranium-text-documents.js.map +1 -1
  13. package/lib/documents/language-client-text-shadow.d.ts +41 -9
  14. package/lib/documents/language-client-text-shadow.d.ts.map +1 -1
  15. package/lib/documents/language-client-text-shadow.js +56 -11
  16. package/lib/documents/language-client-text-shadow.js.map +1 -1
  17. package/lib/documents/self-save-registry.d.ts +37 -6
  18. package/lib/documents/self-save-registry.d.ts.map +1 -1
  19. package/lib/documents/self-save-registry.js +28 -15
  20. package/lib/documents/self-save-registry.js.map +1 -1
  21. package/lib/index.d.ts +2 -0
  22. package/lib/index.d.ts.map +1 -1
  23. package/lib/index.js +5 -0
  24. package/lib/index.js.map +1 -1
  25. package/lib/langium/ast-extension/ast-node-builder.d.ts +32 -9
  26. package/lib/langium/ast-extension/ast-node-builder.d.ts.map +1 -1
  27. package/lib/langium/ast-extension/ast-node-builder.js +60 -22
  28. package/lib/langium/ast-extension/ast-node-builder.js.map +1 -1
  29. package/lib/langium/bootstrap.d.ts.map +1 -1
  30. package/lib/langium/bootstrap.js +16 -9
  31. package/lib/langium/bootstrap.js.map +1 -1
  32. package/lib/langium/config/configuration-provider.d.ts +81 -0
  33. package/lib/langium/config/configuration-provider.d.ts.map +1 -0
  34. package/lib/langium/config/configuration-provider.js +112 -0
  35. package/lib/langium/config/configuration-provider.js.map +1 -0
  36. package/lib/langium/config/index.d.ts +1 -0
  37. package/lib/langium/config/index.d.ts.map +1 -1
  38. package/lib/langium/config/index.js +1 -0
  39. package/lib/langium/config/index.js.map +1 -1
  40. package/lib/langium/diagnostics/lsp-logger.d.ts +17 -0
  41. package/lib/langium/diagnostics/lsp-logger.d.ts.map +1 -1
  42. package/lib/langium/diagnostics/lsp-logger.js +40 -6
  43. package/lib/langium/diagnostics/lsp-logger.js.map +1 -1
  44. package/lib/langium/document-builder/build-pipeline-integration.d.ts +13 -1
  45. package/lib/langium/document-builder/build-pipeline-integration.d.ts.map +1 -1
  46. package/lib/langium/document-builder/build-pipeline-integration.js +1 -1
  47. package/lib/langium/document-builder/build-pipeline-integration.js.map +1 -1
  48. package/lib/langium/document-builder/build-session.d.ts +93 -0
  49. package/lib/langium/document-builder/build-session.d.ts.map +1 -0
  50. package/lib/langium/document-builder/build-session.js +72 -0
  51. package/lib/langium/document-builder/build-session.js.map +1 -0
  52. package/lib/langium/document-builder/document-builder.d.ts +150 -7
  53. package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
  54. package/lib/langium/document-builder/document-builder.js +298 -11
  55. package/lib/langium/document-builder/document-builder.js.map +1 -1
  56. package/lib/langium/document-builder/index.d.ts +1 -0
  57. package/lib/langium/document-builder/index.d.ts.map +1 -1
  58. package/lib/langium/document-builder/index.js +1 -0
  59. package/lib/langium/document-builder/index.js.map +1 -1
  60. package/lib/langium/integration-services.d.ts +45 -1
  61. package/lib/langium/integration-services.d.ts.map +1 -1
  62. package/lib/langium/integration-services.js +10 -5
  63. package/lib/langium/integration-services.js.map +1 -1
  64. package/lib/langium/integrity/integrity-service.d.ts +5 -0
  65. package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
  66. package/lib/langium/integrity/integrity-service.js +34 -1
  67. package/lib/langium/integrity/integrity-service.js.map +1 -1
  68. package/lib/langium/keys/containment.d.ts +64 -0
  69. package/lib/langium/keys/containment.d.ts.map +1 -0
  70. package/lib/langium/keys/containment.js +53 -0
  71. package/lib/langium/keys/containment.js.map +1 -0
  72. package/lib/langium/keys/index.d.ts +1 -0
  73. package/lib/langium/keys/index.d.ts.map +1 -1
  74. package/lib/langium/keys/index.js +1 -0
  75. package/lib/langium/keys/index.js.map +1 -1
  76. package/lib/langium/keys/name-based-key-provider.d.ts +4 -0
  77. package/lib/langium/keys/name-based-key-provider.d.ts.map +1 -1
  78. package/lib/langium/keys/name-based-key-provider.js +4 -0
  79. package/lib/langium/keys/name-based-key-provider.js.map +1 -1
  80. package/lib/langium/model-service/model-service.d.ts +149 -10
  81. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  82. package/lib/langium/model-service/model-service.js +120 -93
  83. package/lib/langium/model-service/model-service.js.map +1 -1
  84. package/lib/langium/module.d.ts +84 -15
  85. package/lib/langium/module.d.ts.map +1 -1
  86. package/lib/langium/module.js +26 -12
  87. package/lib/langium/module.js.map +1 -1
  88. package/lib/langium/naming/name-provider.d.ts +25 -4
  89. package/lib/langium/naming/name-provider.d.ts.map +1 -1
  90. package/lib/langium/naming/name-provider.js +2 -1
  91. package/lib/langium/naming/name-provider.js.map +1 -1
  92. package/lib/langium/naming/name-separator-validation.d.ts +23 -0
  93. package/lib/langium/naming/name-separator-validation.d.ts.map +1 -1
  94. package/lib/langium/naming/name-separator-validation.js +30 -1
  95. package/lib/langium/naming/name-separator-validation.js.map +1 -1
  96. package/lib/langium/residency/cst-residency-service.d.ts +31 -9
  97. package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
  98. package/lib/langium/residency/cst-residency-service.js +13 -56
  99. package/lib/langium/residency/cst-residency-service.js.map +1 -1
  100. package/lib/langium/scope/hydranium-scope-provider.d.ts +32 -13
  101. package/lib/langium/scope/hydranium-scope-provider.d.ts.map +1 -1
  102. package/lib/langium/scope/hydranium-scope-provider.js +37 -20
  103. package/lib/langium/scope/hydranium-scope-provider.js.map +1 -1
  104. package/lib/langium/shared-services.d.ts +15 -2
  105. package/lib/langium/shared-services.d.ts.map +1 -1
  106. package/lib/langium/shared-services.js.map +1 -1
  107. package/lib/langium/transfer/transfer-encoder.d.ts +60 -17
  108. package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
  109. package/lib/langium/transfer/transfer-encoder.js +38 -16
  110. package/lib/langium/transfer/transfer-encoder.js.map +1 -1
  111. package/lib/langium/validation/document-validator.d.ts +294 -10
  112. package/lib/langium/validation/document-validator.d.ts.map +1 -1
  113. package/lib/langium/validation/document-validator.js +448 -5
  114. package/lib/langium/validation/document-validator.js.map +1 -1
  115. package/lib/langium/workspace/document-uri-policy.d.ts +3 -4
  116. package/lib/langium/workspace/document-uri-policy.d.ts.map +1 -1
  117. package/lib/langium/workspace/document-uri-policy.js +3 -4
  118. package/lib/langium/workspace/document-uri-policy.js.map +1 -1
  119. package/lib/langium/workspace/hydranium-langium-document-factory.d.ts +25 -0
  120. package/lib/langium/workspace/hydranium-langium-document-factory.d.ts.map +1 -1
  121. package/lib/langium/workspace/hydranium-langium-document-factory.js +25 -0
  122. package/lib/langium/workspace/hydranium-langium-document-factory.js.map +1 -1
  123. package/lib/langium/workspace/hydranium-workspace-manager.d.ts +21 -1
  124. package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
  125. package/lib/langium/workspace/hydranium-workspace-manager.js +28 -0
  126. package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
  127. package/lib/langium/workspace/in-memory-file-system-provider.d.ts +8 -0
  128. package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
  129. package/lib/langium/workspace/in-memory-file-system-provider.js +11 -2
  130. package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
  131. package/lib/langium/workspace/initialize-workspace.d.ts +22 -2
  132. package/lib/langium/workspace/initialize-workspace.d.ts.map +1 -1
  133. package/lib/langium/workspace/initialize-workspace.js +13 -5
  134. package/lib/langium/workspace/initialize-workspace.js.map +1 -1
  135. package/lib/langium/workspace/langium-documents.d.ts +79 -17
  136. package/lib/langium/workspace/langium-documents.d.ts.map +1 -1
  137. package/lib/langium/workspace/langium-documents.js +83 -28
  138. package/lib/langium/workspace/langium-documents.js.map +1 -1
  139. package/lib/locale/index.d.ts +10 -0
  140. package/lib/locale/index.d.ts.map +1 -0
  141. package/lib/locale/index.js +10 -0
  142. package/lib/locale/index.js.map +1 -0
  143. package/lib/locale/server-locale.d.ts +73 -0
  144. package/lib/locale/server-locale.d.ts.map +1 -0
  145. package/lib/locale/server-locale.js +61 -0
  146. package/lib/locale/server-locale.js.map +1 -0
  147. package/lib/lsp/hydranium-document-update-handler.js +1 -1
  148. package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
  149. package/lib/lsp/index.d.ts +1 -1
  150. package/lib/lsp/index.d.ts.map +1 -1
  151. package/lib/lsp/index.js +1 -1
  152. package/lib/lsp/index.js.map +1 -1
  153. package/lib/lsp/lsp-latency.d.ts +39 -0
  154. package/lib/lsp/lsp-latency.d.ts.map +1 -0
  155. package/lib/lsp/lsp-latency.js +58 -0
  156. package/lib/lsp/lsp-latency.js.map +1 -0
  157. package/lib/lsp/semantic-token-provider.d.ts +34 -1
  158. package/lib/lsp/semantic-token-provider.d.ts.map +1 -1
  159. package/lib/lsp/semantic-token-provider.js +55 -8
  160. package/lib/lsp/semantic-token-provider.js.map +1 -1
  161. package/lib/messages/carriers.d.ts +54 -0
  162. package/lib/messages/carriers.d.ts.map +1 -0
  163. package/lib/messages/carriers.js +62 -0
  164. package/lib/messages/carriers.js.map +1 -0
  165. package/lib/messages/index.d.ts +25 -0
  166. package/lib/messages/index.d.ts.map +1 -0
  167. package/lib/messages/index.js +25 -0
  168. package/lib/messages/index.js.map +1 -0
  169. package/lib/messages/renderer.d.ts +136 -0
  170. package/lib/messages/renderer.d.ts.map +1 -0
  171. package/lib/messages/renderer.js +159 -0
  172. package/lib/messages/renderer.js.map +1 -0
  173. package/lib/node/heap-ceiling.d.ts +61 -0
  174. package/lib/node/heap-ceiling.d.ts.map +1 -0
  175. package/lib/node/heap-ceiling.js +72 -0
  176. package/lib/node/heap-ceiling.js.map +1 -0
  177. package/lib/node/index.d.ts +1 -0
  178. package/lib/node/index.d.ts.map +1 -1
  179. package/lib/node/index.js +1 -0
  180. package/lib/node/index.js.map +1 -1
  181. package/lib/node/latency-from-env.d.ts +1 -1
  182. package/lib/node/latency-from-env.js +1 -1
  183. package/lib/node/node-file-system-provider.d.ts.map +1 -1
  184. package/lib/node/node-file-system-provider.js +1 -40
  185. package/lib/node/node-file-system-provider.js.map +1 -1
  186. package/lib/node/profiling-run.d.ts +2 -2
  187. package/lib/node/profiling-run.js +1 -1
  188. package/lib/node/profiling-run.js.map +1 -1
  189. package/lib/node/rename-over-open-readers.d.ts +52 -0
  190. package/lib/node/rename-over-open-readers.d.ts.map +1 -0
  191. package/lib/node/rename-over-open-readers.js +66 -0
  192. package/lib/node/rename-over-open-readers.js.map +1 -0
  193. package/lib/testing/fake-document.d.ts +16 -4
  194. package/lib/testing/fake-document.d.ts.map +1 -1
  195. package/lib/testing/fake-document.js +18 -3
  196. package/lib/testing/fake-document.js.map +1 -1
  197. package/lib/testing/make-noop-shared-services.d.ts +14 -0
  198. package/lib/testing/make-noop-shared-services.d.ts.map +1 -1
  199. package/lib/testing/make-noop-shared-services.js +28 -2
  200. package/lib/testing/make-noop-shared-services.js.map +1 -1
  201. package/lib/testing/make-test-services.d.ts +44 -8
  202. package/lib/testing/make-test-services.d.ts.map +1 -1
  203. package/lib/testing/make-test-services.js +19 -3
  204. package/lib/testing/make-test-services.js.map +1 -1
  205. package/lib/testing/node/spawned-server.d.ts +14 -0
  206. package/lib/testing/node/spawned-server.d.ts.map +1 -1
  207. package/lib/testing/node/spawned-server.js +33 -1
  208. package/lib/testing/node/spawned-server.js.map +1 -1
  209. package/lib/testing/playwright/flaky-network-proxy.d.ts +76 -0
  210. package/lib/testing/playwright/flaky-network-proxy.d.ts.map +1 -0
  211. package/lib/testing/playwright/flaky-network-proxy.js +164 -0
  212. package/lib/testing/playwright/flaky-network-proxy.js.map +1 -0
  213. package/lib/testing/playwright/index.d.ts +1 -0
  214. package/lib/testing/playwright/index.d.ts.map +1 -1
  215. package/lib/testing/playwright/index.js +1 -0
  216. package/lib/testing/playwright/index.js.map +1 -1
  217. package/lib/testing/playwright/server-log-capture.d.ts +14 -4
  218. package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
  219. package/lib/testing/playwright/server-log-capture.js +21 -4
  220. package/lib/testing/playwright/server-log-capture.js.map +1 -1
  221. package/lib/testing/playwright/server-log-rename-reporter.d.ts.map +1 -1
  222. package/lib/testing/playwright/server-log-rename-reporter.js +10 -1
  223. package/lib/testing/playwright/server-log-rename-reporter.js.map +1 -1
  224. package/lib/testing/stub-ast-document-manager.d.ts +5 -5
  225. package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
  226. package/lib/testing/stub-ast-document-manager.js +4 -6
  227. package/lib/testing/stub-ast-document-manager.js.map +1 -1
  228. package/lib/testing/stub-document-builder.d.ts +8 -0
  229. package/lib/testing/stub-document-builder.d.ts.map +1 -1
  230. package/lib/testing/stub-document-builder.js +22 -3
  231. package/lib/testing/stub-document-builder.js.map +1 -1
  232. package/lib/testing/stub-langium-documents.d.ts +3 -2
  233. package/lib/testing/stub-langium-documents.d.ts.map +1 -1
  234. package/lib/testing/stub-langium-documents.js.map +1 -1
  235. package/lib/testing/stub-model-service.d.ts +5 -4
  236. package/lib/testing/stub-model-service.d.ts.map +1 -1
  237. package/lib/testing/stub-model-service.js +2 -2
  238. package/lib/testing/stub-model-service.js.map +1 -1
  239. package/package.json +17 -8
  240. package/src/documents/ast-document-manager.ts +129 -36
  241. package/src/documents/client-ids.ts +5 -22
  242. package/src/documents/hydranium-text-documents.ts +167 -61
  243. package/src/documents/language-client-text-shadow.ts +59 -11
  244. package/src/documents/self-save-registry.ts +52 -13
  245. package/src/index.ts +5 -0
  246. package/src/langium/ast-extension/ast-node-builder.ts +66 -22
  247. package/src/langium/bootstrap.ts +16 -9
  248. package/src/langium/config/configuration-provider.ts +119 -0
  249. package/src/langium/config/index.ts +1 -0
  250. package/src/langium/diagnostics/lsp-logger.ts +41 -6
  251. package/src/langium/document-builder/build-pipeline-integration.ts +14 -1
  252. package/src/langium/document-builder/build-session.ts +87 -0
  253. package/src/langium/document-builder/document-builder.ts +339 -11
  254. package/src/langium/document-builder/index.ts +1 -0
  255. package/src/langium/integration-services.ts +67 -6
  256. package/src/langium/integrity/integrity-service.ts +35 -1
  257. package/src/langium/keys/containment.ts +87 -0
  258. package/src/langium/keys/index.ts +1 -0
  259. package/src/langium/keys/name-based-key-provider.ts +4 -0
  260. package/src/langium/model-service/model-service.ts +211 -30
  261. package/src/langium/module.ts +106 -21
  262. package/src/langium/naming/name-provider.ts +27 -5
  263. package/src/langium/naming/name-separator-validation.ts +34 -5
  264. package/src/langium/residency/cst-residency-service.ts +36 -12
  265. package/src/langium/scope/hydranium-scope-provider.ts +40 -22
  266. package/src/langium/shared-services.ts +19 -2
  267. package/src/langium/transfer/transfer-encoder.ts +85 -24
  268. package/src/langium/validation/document-validator.ts +537 -11
  269. package/src/langium/workspace/document-uri-policy.ts +3 -4
  270. package/src/langium/workspace/hydranium-langium-document-factory.ts +34 -0
  271. package/src/langium/workspace/hydranium-workspace-manager.ts +30 -1
  272. package/src/langium/workspace/in-memory-file-system-provider.ts +13 -3
  273. package/src/langium/workspace/initialize-workspace.ts +34 -5
  274. package/src/langium/workspace/langium-documents.ts +106 -33
  275. package/src/locale/index.ts +10 -0
  276. package/src/locale/server-locale.ts +86 -0
  277. package/src/lsp/hydranium-document-update-handler.ts +1 -1
  278. package/src/lsp/index.ts +1 -1
  279. package/src/lsp/lsp-latency.ts +62 -0
  280. package/src/lsp/semantic-token-provider.ts +74 -7
  281. package/src/messages/carriers.ts +78 -0
  282. package/src/messages/index.ts +36 -0
  283. package/src/messages/renderer.ts +204 -0
  284. package/src/node/heap-ceiling.ts +108 -0
  285. package/src/node/index.ts +1 -0
  286. package/src/node/latency-from-env.ts +1 -1
  287. package/src/node/node-file-system-provider.ts +1 -41
  288. package/src/node/profiling-run.ts +3 -3
  289. package/src/node/rename-over-open-readers.ts +78 -0
  290. package/src/testing/fake-document.ts +24 -5
  291. package/src/testing/make-noop-shared-services.ts +50 -2
  292. package/src/testing/make-test-services.ts +73 -18
  293. package/src/testing/node/spawned-server.ts +35 -1
  294. package/src/testing/playwright/flaky-network-proxy.ts +240 -0
  295. package/src/testing/playwright/index.ts +1 -0
  296. package/src/testing/playwright/server-log-capture.ts +24 -5
  297. package/src/testing/playwright/server-log-rename-reporter.ts +12 -1
  298. package/src/testing/stub-ast-document-manager.ts +10 -14
  299. package/src/testing/stub-document-builder.ts +32 -3
  300. package/src/testing/stub-langium-documents.ts +3 -2
  301. package/src/testing/stub-model-service.ts +7 -6
  302. package/lib/lsp/instrument-connection.d.ts +0 -36
  303. package/lib/lsp/instrument-connection.d.ts.map +0 -1
  304. package/lib/lsp/instrument-connection.js +0 -62
  305. package/lib/lsp/instrument-connection.js.map +0 -1
  306. package/src/lsp/instrument-connection.ts +0 -66
@@ -0,0 +1,87 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /**
11
+ * Containment-array maintenance for callers that build or delete AST nodes
12
+ * programmatically — a GLSP operation handler being the usual one.
13
+ *
14
+ * **These live beside the key provider because the constraint is the key
15
+ * provider's.** `NameBasedKeyProvider` keys a node the `NameProvider` gives no
16
+ * name from its position, as `` `${$containerProperty}@${$containerIndex}` ``,
17
+ * so a node appended with a bare `array.push` carries neither and
18
+ * `getElementKey` answers `undefined` — the element has no key at all, and the
19
+ * GLSP index cannot address it. Deleting from the middle of such a list must
20
+ * renumber the survivors, or an id derived later in the same command names the
21
+ * wrong node. Shipping the constraint's statement without the code that
22
+ * discharges it leaves every adopter in that position re-deriving thirty lines
23
+ * from prose, and both failures are silent.
24
+ *
25
+ * The population is narrower than "anyone creating elements from a diagram": it
26
+ * takes a grammar with UNNAMED element types, programmatic creation or deletion
27
+ * of them, and this key provider. A grammar whose diagram elements are all
28
+ * named never reaches it, and the two ways out are named on the provider —
29
+ * give the type a name, or bind a provider whose fallback is content-derived.
30
+ *
31
+ * **Free functions, and deliberately not a step on the AST builder.** Building a
32
+ * node detached and appending it later is legitimate, so a containment step
33
+ * cannot be mandatory; and an optional fluent step could not check the index
34
+ * against the array unless it were handed the array, at which point it is these
35
+ * signatures with more ceremony. `AstNodeInit` already accepts the three fields
36
+ * for a caller who has them, which is the case these cover: it is the INDEX that
37
+ * cannot be known without reading the array.
38
+ */
39
+
40
+ import type { AstNode } from '@hydranium/langium';
41
+ import type { Mutable } from '@hydranium/protocol';
42
+
43
+ /**
44
+ * Append `child` to `children`, stamping the Langium containment plumbing
45
+ * (`$container`, `$containerProperty`, `$containerIndex`) so a positional key
46
+ * can address it.
47
+ *
48
+ * `property` and `children` are separate parameters rather than one property
49
+ * name the function dereferences: the array is what the index is derived from,
50
+ * so taking it directly is what makes the two agree by construction instead of
51
+ * by a lookup that a typo could point elsewhere.
52
+ *
53
+ * @returns `child`, so a caller can append and use it in one expression.
54
+ */
55
+ export function appendChild<TChild extends AstNode>(container: AstNode, property: string, children: TChild[], child: TChild): TChild {
56
+ const mutable = child as Mutable<TChild>;
57
+ mutable.$container = container;
58
+ mutable.$containerProperty = property;
59
+ mutable.$containerIndex = children.length;
60
+ children.push(child);
61
+ return child;
62
+ }
63
+
64
+ /**
65
+ * Remove every entry of `children` that is in `toRemove`, then renumber the
66
+ * survivors' `$containerIndex`.
67
+ *
68
+ * Mutates `children` in place rather than returning a new array, because the
69
+ * caller's array IS the AST's containment list — replacing the reference would
70
+ * leave the parent pointing at the old one.
71
+ *
72
+ * @returns how many entries were removed, so a caller can tell a no-op delete
73
+ * from one that changed the model without comparing lengths itself.
74
+ */
75
+ export function removeChildren<TChild extends AstNode>(children: TChild[], toRemove: ReadonlySet<TChild>): number {
76
+ if (toRemove.size === 0) {
77
+ return 0;
78
+ }
79
+ const survivors = children.filter(child => !toRemove.has(child));
80
+ const removed = children.length - survivors.length;
81
+ children.length = 0;
82
+ for (const [index, child] of survivors.entries()) {
83
+ (child as Mutable<TChild>).$containerIndex = index;
84
+ children.push(child);
85
+ }
86
+ return removed;
87
+ }
@@ -7,6 +7,7 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
+ export * from './containment.js';
10
11
  export * from './element-key-provider.js';
11
12
  export * from './name-based-key-provider.js';
12
13
  export * from './positional-key-provider.js';
@@ -55,6 +55,10 @@ import { type NameProvider } from '../naming/name-provider.js';
55
55
  * - After a removal, surviving siblings need their `$containerIndex`
56
56
  * renumbered, or a key derived afterwards addresses the wrong node.
57
57
  *
58
+ * {@link appendChild} and {@link removeChildren} discharge both, and ship
59
+ * beside this provider because the constraint is this provider's rather than
60
+ * any caller's.
61
+ *
58
62
  * Adopters that need insert / delete stability for unnamed types should
59
63
  * give those types a name in the grammar, or bind a key provider whose
60
64
  * fallback is content-derived rather than positional.
@@ -11,11 +11,12 @@ import {
11
11
  type CanonicalUri,
12
12
  type CloseModelArgs,
13
13
  ConflictError,
14
+ isSnapshotVersion,
15
+ defineMessage,
14
16
  Logger,
15
17
  type MaybeObservableValue,
16
18
  type MaybePromise,
17
19
  ObservableValue,
18
- type TransferDiagnostic,
19
20
  type TransferElement,
20
21
  type OpenModelArgs,
21
22
  type Tracer,
@@ -23,6 +24,7 @@ import {
23
24
  type TransferUpdateArgs
24
25
  } from '@hydranium/protocol';
25
26
  import { type AstNode, DocumentState, type LangiumDocument, UriUtils, type URI } from '@hydranium/langium';
27
+ import { type AstDiagnostic } from '../validation/document-validator.js';
26
28
  import { type DocumentUriPolicy } from '../workspace/document-uri-policy.js';
27
29
  import { ReentrantWriteLockError, isInsideWriteLock } from '../workspace/write-lock-scope.js';
28
30
  import { type CancellationToken, type Disposable } from 'vscode-languageserver';
@@ -34,6 +36,23 @@ import { labelPhaseListener } from '../document-builder/labeled-phase-listener.j
34
36
  import { LANGUAGE_CLIENT_ID } from '../../documents/client-ids.js';
35
37
  import { type ServerSharedServices } from '../module.js';
36
38
 
39
+ /**
40
+ * The undo-stack entry for a server-authored write pushed to the editor.
41
+ *
42
+ * **A user-facing LABEL, not a log string**, which is easy to miss because it
43
+ * travels as an options field rather than as a message: LSP specifies
44
+ * `ApplyWorkspaceEditParams.label` as "presented in the user interface for
45
+ * example on an undo stack to undo the workspace edit". So a user who edits
46
+ * through a form or drags a diagram node reads this in their editor's undo menu
47
+ * — which is why it is rendered like any other message the server sends rather
48
+ * than left as the English literal it was.
49
+ *
50
+ * Parameterless deliberately. The obvious improvement is to name the document,
51
+ * and it is the wrong one: an undo menu is already grouped under the file, so
52
+ * the URI would be noise in the one place it is redundant.
53
+ */
54
+ export const MODEL_UPDATE_EDIT = defineMessage('hydranium/core/model-update-edit', 'Update Model');
55
+
37
56
  /** Max time {@link ModelService.settleSave} waits for the build to settle and the sync chain to drain. */
38
57
  const SAVE_SETTLE_TIMEOUT_MS = 10_000;
39
58
 
@@ -176,15 +195,91 @@ export interface ModelServiceOptions extends LogNameOptions {
176
195
  * **Generic parameters.**
177
196
  * - `TAst` — the AST root type each consumer expects on the returned
178
197
  * {@link AstDocument}. Constrained to {@link AstNode}.
179
- * - `TDiagnostic` — wire diagnostic shape used by the injected
180
- * `TransferEncoder`. Defaults to {@link TransferDiagnostic}.
198
+ * - `TDiagnostic` — the AST-layer diagnostic: whatever the build left on
199
+ * `LangiumDocument.diagnostics`, carried through on the returned
200
+ * {@link AstDocument}. Defaults to {@link AstDiagnostic}.
201
+ * **Not the `TransferEncoder`'s parameter of the same name**, which is
202
+ * that encoder's OUTPUT and so names the wire shape. This one names its
203
+ * input, and an adopter binds the two to different types.
181
204
  * - `TTransfer` — transfer-model root accepted by `update` / `save`
182
205
  * args. Constrained to {@link TransferElement}. Defaults to the
183
206
  * structural base.
184
207
  */
185
- export class ModelService<
208
+ /**
209
+ * The seam every non-LSP head talks to: the data server, the GLSP head and an
210
+ * adopter's own services reach documents through this slot rather than through
211
+ * the workspace stores.
212
+ *
213
+ * **Two families, and the distinction matters more than the names suggest.**
214
+ * `waitFor*` is a pure wait — it never triggers a build, so a caller waiting on
215
+ * a document no build has touched waits until something else builds it.
216
+ * `ensureDocumentState` and the phase shorthands over it *dispatch*: warm
217
+ * documents are awaited, cold ones are built.
218
+ *
219
+ * **Diagnostics are typed `never` below `Validated`.** Validation is the last
220
+ * phase, so at any earlier landmark the array either is not yet computed or
221
+ * still holds the previous build's, and reading it would take stale results
222
+ * for fresh ones. A caller that needs diagnostics asks for {@link validated}.
223
+ *
224
+ * What the `never` buys, exactly: reading a field off an element is a compile
225
+ * error, and nothing can be appended. It does NOT stop a caller assigning an
226
+ * element to a typed variable, because `never` is assignable to everything — so
227
+ * this is a guard against reaching for diagnostics by accident, not a seal
228
+ * against doing it deliberately.
229
+ *
230
+ * The waits resolve at or ABOVE their target, so an already-validated document
231
+ * does carry usable diagnostics and the `never` over-forbids there. That
232
+ * direction is the safe one: the alternative permits stale reads silently. A
233
+ * member taking a phase as a PARAMETER cannot judge statically and so returns
234
+ * `TDiagnostic`, leaving the choice to the caller.
235
+ */
236
+ export interface ModelService<
237
+ TAst extends AstNode,
238
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
239
+ TTransfer extends TransferElement = TransferElement
240
+ > {
241
+ /**
242
+ * Resolves once the workspace has been initialised and its first build has
243
+ * completed — the gate every read should wait behind, since a document
244
+ * queried before it may be unbuilt and reach no phase.
245
+ *
246
+ * A property rather than a method, matching `ProjectManager.ready` and
247
+ * Langium's `WorkspaceManager.ready`. An implementation needing a stricter
248
+ * gate supplies a Promise that awaits its own concern as well.
249
+ */
250
+ readonly ready: Promise<void>;
251
+
252
+ // Pure waits — never trigger a build.
253
+ waitForDocumentState(uri: string, state: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
254
+ waitForDocumentSettled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
255
+ waitForBuilderState(state: DocumentState, cancelToken?: CancellationToken): Promise<void>;
256
+
257
+ // Wait if warm, build if cold.
258
+ ensureDocumentState(uri: string, state?: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
259
+ rebuild(uri: string, state?: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
260
+ parsed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
261
+ linked(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
262
+ settled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
263
+ indexed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
264
+ validated(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
265
+
266
+ update(args: TransferUpdateArgs<TTransfer>, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
267
+ save(args: TransferSaveArgs<TTransfer>, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
268
+
269
+ open(args: OpenModelArgs): Promise<Disposable>;
270
+ close(args: CloseModelArgs): Promise<void>;
271
+ isOpen(uri: string): boolean;
272
+ snapshot(uri: string): AstDocument<TAst, TDiagnostic> | undefined;
273
+ getDocument(uri: string): LangiumDocument | undefined;
274
+
275
+ onModelUpdated(uri: string, listener: (event: AstDocumentUpdatedEvent<TAst, TDiagnostic>) => void): Disposable;
276
+ onModelSaved(uri: string, listener: (event: AstDocumentSavedEvent<TAst, TDiagnostic>) => void): Disposable;
277
+ onClientClosed(uri: string, clientId: string, listener: () => void): Disposable;
278
+ }
279
+
280
+ export class DefaultModelService<
186
281
  TAst extends AstNode,
187
- TDiagnostic = TransferDiagnostic,
282
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
188
283
  /**
189
284
  * Structured payload accepted by `update` / `save`. Constrained to
190
285
  * {@link TransferElement} — the minimal `{ readonly $type: string }`
@@ -195,7 +290,7 @@ export class ModelService<
195
290
  * integrity service) that pass AST roots remain compatible.
196
291
  */
197
292
  TTransfer extends TransferElement = TransferElement
198
- > {
293
+ > implements ModelService<TAst, TDiagnostic, TTransfer> {
199
294
  protected readonly tracer: Tracer;
200
295
  /**
201
296
  * The single document-identity seam (`services.workspace.DocumentUriPolicy`),
@@ -329,8 +424,12 @@ export class ModelService<
329
424
  * {@link waitForDocumentState} for the common "wait until content is stable"
330
425
  * case (e.g. settling a save). Pure wait — does not trigger a build.
331
426
  */
332
- async waitForDocumentSettled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>> {
333
- return this.waitForDocumentStateCanonical(this.uriPolicy.canonicalUri(uri), IntegrityService.SettledState, cancelToken);
427
+ async waitForDocumentSettled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
428
+ return (await this.waitForDocumentStateCanonical(
429
+ this.uriPolicy.canonicalUri(uri),
430
+ IntegrityService.SettledState,
431
+ cancelToken
432
+ )) as AstDocument<TAst, never>;
334
433
  }
335
434
 
336
435
  /**
@@ -484,11 +583,19 @@ export class ModelService<
484
583
  *
485
584
  * Phase invariants encoded in the return type:
486
585
  * - `parsed` / `linked` / `settled` / `indexed` return
487
- * `AstDocument<TAst, never>` — diagnostics array is empty by phase
488
- * contract.
586
+ * `AstDocument<TAst, never>` — no diagnostics have been computed at
587
+ * those phases.
489
588
  * - `validated` returns `AstDocument<TAst, TDiagnostic>` — diagnostics
490
589
  * are populated.
491
590
  *
591
+ * **The `never` is enforced, not merely declared.** The wait underneath
592
+ * resolves at or ABOVE the requested state, so a document something else
593
+ * already carried past `Validated` would otherwise come back from
594
+ * `settled()` carrying a full diagnostics array typed `never`; these four
595
+ * strip it. An empty array here therefore means "this read does not report
596
+ * diagnostics", never "this document is clean" — call {@link validated}
597
+ * when the answer has to mean the second.
598
+ *
492
599
  * `settled` is the integrity-overlay name for "all integrity rules
493
600
  * have fired"; it maps to {@link IntegrityService.SettledState} (which
494
601
  * equals `DocumentState.IndexedReferences`), but the dedicated method
@@ -496,19 +603,19 @@ export class ModelService<
496
603
  * ever moves.
497
604
  */
498
605
  async parsed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
499
- return this.ensureDocumentState(uri, DocumentState.Parsed, cancelToken) as Promise<AstDocument<TAst, never>>;
606
+ return this.withoutDiagnostics(await this.ensureDocumentState(uri, DocumentState.Parsed, cancelToken));
500
607
  }
501
608
 
502
609
  async linked(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
503
- return this.ensureDocumentState(uri, DocumentState.Linked, cancelToken) as Promise<AstDocument<TAst, never>>;
610
+ return this.withoutDiagnostics(await this.ensureDocumentState(uri, DocumentState.Linked, cancelToken));
504
611
  }
505
612
 
506
613
  async settled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
507
- return this.ensureDocumentState(uri, IntegrityService.SettledState, cancelToken) as Promise<AstDocument<TAst, never>>;
614
+ return this.withoutDiagnostics(await this.ensureDocumentState(uri, IntegrityService.SettledState, cancelToken));
508
615
  }
509
616
 
510
617
  async indexed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
511
- return this.ensureDocumentState(uri, DocumentState.IndexedReferences, cancelToken) as Promise<AstDocument<TAst, never>>;
618
+ return this.withoutDiagnostics(await this.ensureDocumentState(uri, DocumentState.IndexedReferences, cancelToken));
512
619
  }
513
620
 
514
621
  async validated(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>> {
@@ -598,19 +705,29 @@ export class ModelService<
598
705
  // created from the payload rather than read from the filesystem — `update`
599
706
  // is an upsert. For an already-open document `open` refreshes content (the
600
707
  // text is ignored on that branch), so existing-document behaviour is
601
- // unchanged. `version` is intentionally NOT forwarded to `open`: a cold
602
- // create stays at its initial version, so a based-on-`version` update of a
603
- // not-yet-existing document still trips the conflict gate below.
708
+ // unchanged. `version` is intentionally NOT forwarded to `open`, so a cold
709
+ // create stays at its initial version rather than adopting a number the
710
+ // caller chose.
711
+ //
712
+ // The gate's version is read BEFORE that open, and must be: for a document
713
+ // no client holds open, the open assigns the shared version from the
714
+ // INCOMING text, so a version read afterwards has already absorbed the
715
+ // caller's own write. Gating on it rejected every modifying write to a
716
+ // closed document, having compared the caller's `basedOn` against a
717
+ // number the caller itself produced — and a serialised round-trip that is
718
+ // not byte-identical to the stored text was enough to trigger it. Reading
719
+ // first keeps both cases the gate exists for: an unknown URI answers 0, so
720
+ // a based-on-version update of a not-yet-existing document still trips it,
721
+ // and a genuine conflict still trips it, another writer having advanced the
722
+ // sequence past the version the caller read.
723
+ const currentVersion = this.services.workspace.TextDocuments.version(uri);
604
724
  const text = await run('serialize', () => this.modelToText(uri, args.model, cancelToken));
605
725
  await run('open', () => this.open({ uri, clientId: args.clientId, text }));
606
- if (args.baseVersion !== undefined) {
607
- const current = this.services.workspace.TextDocuments.version(uri);
608
- if (current !== args.baseVersion) {
609
- // Distinct from the post-build "superseded" debug line below: this is a
610
- // based-on-stale rejection (the write never applies), not two writes racing.
611
- this.tracer.debug(`Conflict on ${uri}: based-on v${args.baseVersion} stale, server at v${current}`);
612
- throw new ConflictError(uri, args.baseVersion, current);
613
- }
726
+ if (isSnapshotVersion(args.basedOn) && currentVersion !== args.basedOn) {
727
+ // Distinct from the post-build "superseded" debug line below: this is a
728
+ // based-on-stale rejection (the write never applies), not two writes racing.
729
+ this.tracer.debug(`Conflict on ${uri}: based-on v${args.basedOn} stale, server at v${currentVersion}`);
730
+ throw new ConflictError(uri, args.basedOn, currentVersion);
614
731
  }
615
732
  const appliedVersion = await run('apply', () => this.services.workspace.AstDocumentManager.update(uri, text, args.clientId));
616
733
  // Dispatch through the public `rebuild` (which re-canonicalizes the already-
@@ -733,6 +850,31 @@ export class ModelService<
733
850
  return this.services.workspace.AstDocumentManager.isOpen(uri);
734
851
  }
735
852
 
853
+ /**
854
+ * Snapshot of `uri` as it stands RIGHT NOW — the synchronous sibling of the
855
+ * phase reads, which all wait. `undefined` when no document is registered.
856
+ *
857
+ * **This is what a writer wants, and {@link getDocument} is not.** The
858
+ * envelope's `version` is copied by value at projection time, so it cannot
859
+ * move afterwards; a version read off the live document at write time is
860
+ * whatever the server is at *now*, which is the number an optimistic gate is
861
+ * about to compare it against.
862
+ *
863
+ * Diagnostics only from a document that has reached `Validated`, and an
864
+ * empty array otherwise. Unlike the phase reads this one names no phase, so
865
+ * the state it finds is the only thing that can say whether the array
866
+ * describes the content being handed back or whatever an earlier build left.
867
+ * A caller that needs them unconditionally waits, via {@link validated}.
868
+ */
869
+ snapshot(uri: string): AstDocument<TAst, TDiagnostic> | undefined {
870
+ const document = this.getDocument(uri);
871
+ if (!document) {
872
+ return undefined;
873
+ }
874
+ const envelope = AstDocument.from<TAst, TDiagnostic>(document);
875
+ return document.state >= DocumentState.Validated ? envelope : this.withoutDiagnostics(envelope);
876
+ }
877
+
736
878
  /**
737
879
  * The built {@link LangiumDocument} for `uri`, looked up by canonical identity —
738
880
  * the synchronous, phase-agnostic sibling of {@link ensureDocumentState} /
@@ -741,6 +883,13 @@ export class ModelService<
741
883
  * into `LangiumDocuments` directly: a symlinked / `..` / case-divergent URI
742
884
  * still resolves to the one document the build keys by its real path. Returns
743
885
  * `undefined` if no document is registered for `uri`.
886
+ *
887
+ * **Live, so do not take a based-on version off it.** `textDocument` is the
888
+ * store's own object rather than a copy, so `.version` read here answers for
889
+ * the moment of the READ, not the moment of the earlier content — pass it to
890
+ * a write and the server compares its current version against itself, the
891
+ * gate passes unconditionally, and a concurrent edit is overwritten with
892
+ * nothing logged. Use {@link snapshot} for that, or a phase read.
744
893
  */
745
894
  getDocument(uri: string): LangiumDocument | undefined {
746
895
  return this.services.workspace.AstDocumentManager.getDocument(uri);
@@ -834,17 +983,17 @@ export class ModelService<
834
983
  *
835
984
  * The signal is "a known client other than the language client authored this
836
985
  * version **and** the URI was in the last build's changed set
837
- * (`isDirectChange`)". A framework-internal rebuild reports no author
986
+ * (`isTriggeringEdit`)". A framework-internal rebuild reports no author
838
987
  * (`getAuthor` → `undefined`), so it fails `hasKnownAuthor` without comparing
839
988
  * against a sentinel. This is NOT redundant with content/registration — it
840
989
  * distinguishes "client edited" from "framework rebuilt", which neither the
841
- * shadow nor `isDirectChange` alone can.
990
+ * shadow nor `isTriggeringEdit` alone can.
842
991
  */
843
992
  protected isNonLanguageClientEdit(document: LangiumDocument): boolean {
844
993
  const documents = this.services.workspace.AstDocumentManager;
845
994
  const author = documents.getAuthor(document);
846
995
  const hasKnownAuthor = !!author && author !== LANGUAGE_CLIENT_ID;
847
- return hasKnownAuthor && documents.isDirectChange(document.textDocument.uri);
996
+ return hasKnownAuthor && documents.isTriggeringEdit(document.textDocument.uri);
848
997
  }
849
998
 
850
999
  /**
@@ -878,13 +1027,26 @@ export class ModelService<
878
1027
  this.syncChains.set(uri, chain);
879
1028
  }
880
1029
 
1030
+ /**
1031
+ * The undo-stack label for a server-authored write, in the locale the server
1032
+ * was handed at init.
1033
+ *
1034
+ * One method rather than the literal at each `applyEdit`, because the two
1035
+ * call sites are the same edit — a push and its full-replace retry — and an
1036
+ * undo menu showing two different words for one operation would read as two
1037
+ * operations.
1038
+ */
1039
+ protected editLabel(): string {
1040
+ return this.services.MessageRenderer.renderMessage(MODEL_UPDATE_EDIT);
1041
+ }
1042
+
881
1043
  protected async drainSyncQueue(uri: string): Promise<void> {
882
1044
  const uriLogger = this.tracer.withUri(uri);
883
1045
  while (this.pendingSync.has(uri)) {
884
1046
  const text = this.pendingSync.get(uri)!;
885
1047
  this.pendingSync.delete(uri);
886
1048
  try {
887
- let result = await this.services.workspace.TextDocuments.applyEditToLanguageClient(uri, text, { label: 'Update Model' });
1049
+ let result = await this.services.workspace.TextDocuments.applyEditToLanguageClient(uri, text, { label: this.editLabel() });
888
1050
  if (result?.applied === false && !this.pendingSync.has(uri)) {
889
1051
  // The push is addressed at the client's LAST DECLARED VERSION, so a
890
1052
  // rejection normally means the client's buffer moved while the
@@ -905,7 +1067,7 @@ export class ModelService<
905
1067
  // queued — best-effort, since a settle arriving later simply pushes
906
1068
  // after this and still wins.
907
1069
  uriLogger.warn(`Language client rejected applyEdit at its declared version — re-pushing a full replace`);
908
- result = await this.services.workspace.TextDocuments.applyEditToLanguageClient(uri, text, { label: 'Update Model' });
1070
+ result = await this.services.workspace.TextDocuments.applyEditToLanguageClient(uri, text, { label: this.editLabel() });
909
1071
  if (result?.applied === false) {
910
1072
  uriLogger.warn(`Language client rejected the full-replace retry too — client content is stale`);
911
1073
  }
@@ -1048,4 +1210,23 @@ export class ModelService<
1048
1210
  ? AstDocument.from<TAst, TDiagnostic>(document)
1049
1211
  : AstDocument.create<TAst, TDiagnostic>(uri.toString(), 0, undefined as unknown as TAst, []);
1050
1212
  }
1213
+
1214
+ /**
1215
+ * The same envelope with no diagnostics, for a read that names a phase below
1216
+ * `Validated`.
1217
+ *
1218
+ * Langium fills `LangiumDocument.diagnostics` from inside `validateDocument`
1219
+ * and from nowhere else, so below that phase the array holds whatever an
1220
+ * EARLIER build left — a verdict about text the document may no longer have.
1221
+ * The wait underneath resolves at or above the phase asked for, so a document
1222
+ * something else carried past `Validated` would otherwise hand a full array
1223
+ * back from `parsed()`.
1224
+ *
1225
+ * A copy rather than a clear: the envelope is freshly built here, but
1226
+ * {@link toAstDocument} is overridable and an adopter's version may return
1227
+ * one it also keeps.
1228
+ */
1229
+ protected withoutDiagnostics(document: AstDocument<TAst, TDiagnostic>): AstDocument<TAst, never> {
1230
+ return { ...document, diagnostics: [] };
1231
+ }
1051
1232
  }