@hydranium/core 1.0.0-next.39 → 1.0.0-next.391

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 (805) hide show
  1. package/README.md +78 -82
  2. package/lib/browser/host-heads-in-worker.d.ts +77 -0
  3. package/lib/browser/host-heads-in-worker.d.ts.map +1 -0
  4. package/lib/browser/host-heads-in-worker.js +169 -0
  5. package/lib/browser/host-heads-in-worker.js.map +1 -0
  6. package/lib/browser/index.d.ts +12 -0
  7. package/lib/browser/index.d.ts.map +1 -0
  8. package/lib/browser/index.js +17 -0
  9. package/lib/browser/index.js.map +1 -0
  10. package/lib/browser/indexeddb-file-system-store.d.ts +82 -0
  11. package/lib/browser/indexeddb-file-system-store.d.ts.map +1 -0
  12. package/lib/browser/indexeddb-file-system-store.js +181 -0
  13. package/lib/browser/indexeddb-file-system-store.js.map +1 -0
  14. package/lib/browser/start-language-server-in-worker.d.ts +32 -0
  15. package/lib/browser/start-language-server-in-worker.d.ts.map +1 -0
  16. package/lib/browser/start-language-server-in-worker.js +40 -0
  17. package/lib/browser/start-language-server-in-worker.js.map +1 -0
  18. package/lib/documents/ast-document-manager.d.ts +254 -140
  19. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  20. package/lib/documents/ast-document-manager.js +311 -128
  21. package/lib/documents/ast-document-manager.js.map +1 -1
  22. package/lib/documents/client-ids.d.ts +5 -19
  23. package/lib/documents/client-ids.d.ts.map +1 -1
  24. package/lib/documents/client-ids.js +5 -19
  25. package/lib/documents/client-ids.js.map +1 -1
  26. package/lib/documents/client-session-errors.d.ts +15 -0
  27. package/lib/documents/client-session-errors.d.ts.map +1 -0
  28. package/lib/documents/client-session-errors.js +15 -0
  29. package/lib/documents/client-session-errors.js.map +1 -0
  30. package/lib/documents/client-session-registry.d.ts +146 -0
  31. package/lib/documents/client-session-registry.d.ts.map +1 -0
  32. package/lib/documents/client-session-registry.js +207 -0
  33. package/lib/documents/client-session-registry.js.map +1 -0
  34. package/lib/documents/dirty-state-tracker.d.ts +101 -0
  35. package/lib/documents/dirty-state-tracker.d.ts.map +1 -0
  36. package/lib/documents/dirty-state-tracker.js +85 -0
  37. package/lib/documents/dirty-state-tracker.js.map +1 -0
  38. package/lib/documents/document-release-handler.d.ts +204 -0
  39. package/lib/documents/document-release-handler.d.ts.map +1 -0
  40. package/lib/documents/document-release-handler.js +317 -0
  41. package/lib/documents/document-release-handler.js.map +1 -0
  42. package/lib/documents/document-release-scheduler.d.ts +67 -0
  43. package/lib/documents/document-release-scheduler.d.ts.map +1 -0
  44. package/lib/documents/document-release-scheduler.js +80 -0
  45. package/lib/documents/document-release-scheduler.js.map +1 -0
  46. package/lib/documents/file-system-task-queue.d.ts +52 -0
  47. package/lib/documents/file-system-task-queue.d.ts.map +1 -0
  48. package/lib/documents/file-system-task-queue.js +40 -0
  49. package/lib/documents/file-system-task-queue.js.map +1 -0
  50. package/lib/documents/hydranium-text-documents.d.ts +453 -282
  51. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  52. package/lib/documents/hydranium-text-documents.js +918 -504
  53. package/lib/documents/hydranium-text-documents.js.map +1 -1
  54. package/lib/documents/index.d.ts +10 -2
  55. package/lib/documents/index.d.ts.map +1 -1
  56. package/lib/documents/index.js +10 -2
  57. package/lib/documents/index.js.map +1 -1
  58. package/lib/documents/language-client-shadow.d.ts +234 -0
  59. package/lib/documents/language-client-shadow.d.ts.map +1 -0
  60. package/lib/documents/language-client-shadow.js +306 -0
  61. package/lib/documents/language-client-shadow.js.map +1 -0
  62. package/lib/documents/model-ledger.d.ts +47 -0
  63. package/lib/documents/model-ledger.d.ts.map +1 -0
  64. package/lib/documents/model-ledger.js +30 -0
  65. package/lib/documents/model-ledger.js.map +1 -0
  66. package/lib/documents/text-ledger.d.ts +64 -0
  67. package/lib/documents/text-ledger.d.ts.map +1 -0
  68. package/lib/documents/text-ledger.js +63 -0
  69. package/lib/documents/text-ledger.js.map +1 -0
  70. package/lib/documents/version-sync-service.d.ts +153 -0
  71. package/lib/documents/version-sync-service.d.ts.map +1 -0
  72. package/lib/documents/version-sync-service.js +186 -0
  73. package/lib/documents/version-sync-service.js.map +1 -0
  74. package/lib/index.d.ts +3 -0
  75. package/lib/index.d.ts.map +1 -1
  76. package/lib/index.js +3 -0
  77. package/lib/index.js.map +1 -1
  78. package/lib/langium/ast-extension/ast-extension-service.d.ts +42 -31
  79. package/lib/langium/ast-extension/ast-extension-service.d.ts.map +1 -1
  80. package/lib/langium/ast-extension/ast-extension-service.js +45 -19
  81. package/lib/langium/ast-extension/ast-extension-service.js.map +1 -1
  82. package/lib/langium/ast-extension/ast-node-builder.d.ts +12 -7
  83. package/lib/langium/ast-extension/ast-node-builder.d.ts.map +1 -1
  84. package/lib/langium/ast-extension/ast-node-builder.js +25 -9
  85. package/lib/langium/ast-extension/ast-node-builder.js.map +1 -1
  86. package/lib/langium/bootstrap.d.ts +49 -36
  87. package/lib/langium/bootstrap.d.ts.map +1 -1
  88. package/lib/langium/bootstrap.js +75 -36
  89. package/lib/langium/bootstrap.js.map +1 -1
  90. package/lib/langium/build-phase-pass/build-phase-pass-service.d.ts +9 -9
  91. package/lib/langium/build-phase-pass/build-phase-pass-service.d.ts.map +1 -1
  92. package/lib/langium/build-phase-pass/build-phase-pass-service.js +25 -16
  93. package/lib/langium/build-phase-pass/build-phase-pass-service.js.map +1 -1
  94. package/lib/langium/build-phase-pass/build-phase-pass.d.ts +20 -8
  95. package/lib/langium/build-phase-pass/build-phase-pass.d.ts.map +1 -1
  96. package/lib/langium/build-phase-pass/build-phase-pass.js +20 -0
  97. package/lib/langium/build-phase-pass/build-phase-pass.js.map +1 -1
  98. package/lib/langium/composite-ast-reflection.d.ts +46 -0
  99. package/lib/langium/composite-ast-reflection.d.ts.map +1 -0
  100. package/lib/langium/composite-ast-reflection.js +97 -0
  101. package/lib/langium/composite-ast-reflection.js.map +1 -0
  102. package/lib/langium/config/configuration-provider.d.ts +81 -0
  103. package/lib/langium/config/configuration-provider.d.ts.map +1 -0
  104. package/lib/langium/config/configuration-provider.js +112 -0
  105. package/lib/langium/config/configuration-provider.js.map +1 -0
  106. package/lib/langium/config/index.d.ts +1 -0
  107. package/lib/langium/config/index.d.ts.map +1 -1
  108. package/lib/langium/config/index.js +1 -0
  109. package/lib/langium/config/index.js.map +1 -1
  110. package/lib/langium/config/settings.js.map +1 -1
  111. package/lib/langium/diagnostics/console-tee.d.ts +27 -0
  112. package/lib/langium/diagnostics/console-tee.d.ts.map +1 -0
  113. package/lib/langium/diagnostics/console-tee.js +141 -0
  114. package/lib/langium/diagnostics/console-tee.js.map +1 -0
  115. package/lib/langium/diagnostics/hydranium-langium-profiler.d.ts +9 -20
  116. package/lib/langium/diagnostics/hydranium-langium-profiler.d.ts.map +1 -1
  117. package/lib/langium/diagnostics/hydranium-langium-profiler.js +15 -24
  118. package/lib/langium/diagnostics/hydranium-langium-profiler.js.map +1 -1
  119. package/lib/langium/diagnostics/log-preamble.js +1 -1
  120. package/lib/langium/diagnostics/log-preamble.js.map +1 -1
  121. package/lib/langium/diagnostics/logger.d.ts +25 -6
  122. package/lib/langium/diagnostics/logger.d.ts.map +1 -1
  123. package/lib/langium/diagnostics/logger.js +49 -17
  124. package/lib/langium/diagnostics/logger.js.map +1 -1
  125. package/lib/langium/diagnostics/lsp-logger.d.ts +25 -0
  126. package/lib/langium/diagnostics/lsp-logger.d.ts.map +1 -1
  127. package/lib/langium/diagnostics/lsp-logger.js +81 -46
  128. package/lib/langium/diagnostics/lsp-logger.js.map +1 -1
  129. package/lib/langium/diagnostics/server-tracer.d.ts.map +1 -1
  130. package/lib/langium/diagnostics/server-tracer.js.map +1 -1
  131. package/lib/langium/document-builder/build-pipeline-integration.d.ts +21 -1
  132. package/lib/langium/document-builder/build-pipeline-integration.d.ts.map +1 -1
  133. package/lib/langium/document-builder/build-pipeline-integration.js +43 -30
  134. package/lib/langium/document-builder/build-pipeline-integration.js.map +1 -1
  135. package/lib/langium/document-builder/build-session.d.ts +6 -5
  136. package/lib/langium/document-builder/build-session.d.ts.map +1 -1
  137. package/lib/langium/document-builder/build-session.js +5 -5
  138. package/lib/langium/document-builder/build-session.js.map +1 -1
  139. package/lib/langium/document-builder/document-builder.d.ts +390 -40
  140. package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
  141. package/lib/langium/document-builder/document-builder.js +635 -102
  142. package/lib/langium/document-builder/document-builder.js.map +1 -1
  143. package/lib/langium/documentation/comment-provider.d.ts.map +1 -1
  144. package/lib/langium/documentation/comment-provider.js.map +1 -1
  145. package/lib/langium/integration-services.d.ts +20 -23
  146. package/lib/langium/integration-services.d.ts.map +1 -1
  147. package/lib/langium/integration-services.js.map +1 -1
  148. package/lib/langium/integrity/integrity-rule.d.ts +22 -8
  149. package/lib/langium/integrity/integrity-rule.d.ts.map +1 -1
  150. package/lib/langium/integrity/integrity-rule.js.map +1 -1
  151. package/lib/langium/integrity/integrity-service.d.ts +217 -29
  152. package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
  153. package/lib/langium/integrity/integrity-service.js +588 -84
  154. package/lib/langium/integrity/integrity-service.js.map +1 -1
  155. package/lib/langium/keys/containment.d.ts +3 -2
  156. package/lib/langium/keys/containment.d.ts.map +1 -1
  157. package/lib/langium/keys/containment.js.map +1 -1
  158. package/lib/langium/keys/name-based-key-provider.d.ts +17 -10
  159. package/lib/langium/keys/name-based-key-provider.d.ts.map +1 -1
  160. package/lib/langium/keys/name-based-key-provider.js +24 -12
  161. package/lib/langium/keys/name-based-key-provider.js.map +1 -1
  162. package/lib/langium/keys/positional-key-provider.d.ts.map +1 -1
  163. package/lib/langium/keys/positional-key-provider.js.map +1 -1
  164. package/lib/langium/labeling/label-provider.d.ts.map +1 -1
  165. package/lib/langium/labeling/label-provider.js.map +1 -1
  166. package/lib/langium/language-module.d.ts +30 -5
  167. package/lib/langium/language-module.d.ts.map +1 -1
  168. package/lib/langium/language-module.js +21 -5
  169. package/lib/langium/language-module.js.map +1 -1
  170. package/lib/langium/model-service/client-session.d.ts +405 -0
  171. package/lib/langium/model-service/client-session.d.ts.map +1 -0
  172. package/lib/langium/model-service/client-session.js +479 -0
  173. package/lib/langium/model-service/client-session.js.map +1 -0
  174. package/lib/langium/model-service/index.d.ts +2 -0
  175. package/lib/langium/model-service/index.d.ts.map +1 -1
  176. package/lib/langium/model-service/index.js +2 -0
  177. package/lib/langium/model-service/index.js.map +1 -1
  178. package/lib/langium/model-service/model-events.d.ts +68 -0
  179. package/lib/langium/model-service/model-events.d.ts.map +1 -0
  180. package/lib/langium/model-service/model-events.js +10 -0
  181. package/lib/langium/model-service/model-events.js.map +1 -0
  182. package/lib/langium/model-service/model-service.d.ts +441 -323
  183. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  184. package/lib/langium/model-service/model-service.js +602 -407
  185. package/lib/langium/model-service/model-service.js.map +1 -1
  186. package/lib/langium/module.d.ts +96 -37
  187. package/lib/langium/module.d.ts.map +1 -1
  188. package/lib/langium/module.js +41 -29
  189. package/lib/langium/module.js.map +1 -1
  190. package/lib/langium/naming/name-provider.d.ts +32 -7
  191. package/lib/langium/naming/name-provider.d.ts.map +1 -1
  192. package/lib/langium/naming/name-provider.js +10 -4
  193. package/lib/langium/naming/name-provider.js.map +1 -1
  194. package/lib/langium/naming/name-separator-validation.d.ts.map +1 -1
  195. package/lib/langium/project/abstract-project-manager.d.ts +184 -13
  196. package/lib/langium/project/abstract-project-manager.d.ts.map +1 -1
  197. package/lib/langium/project/abstract-project-manager.js +336 -16
  198. package/lib/langium/project/abstract-project-manager.js.map +1 -1
  199. package/lib/langium/project/project-change-event.d.ts +22 -3
  200. package/lib/langium/project/project-change-event.d.ts.map +1 -1
  201. package/lib/langium/project/project-manager.d.ts +28 -2
  202. package/lib/langium/project/project-manager.d.ts.map +1 -1
  203. package/lib/langium/project/single-project-manager.d.ts +7 -1
  204. package/lib/langium/project/single-project-manager.d.ts.map +1 -1
  205. package/lib/langium/project/single-project-manager.js +12 -0
  206. package/lib/langium/project/single-project-manager.js.map +1 -1
  207. package/lib/langium/residency/cst-residency-service.d.ts +92 -52
  208. package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
  209. package/lib/langium/residency/cst-residency-service.js +154 -58
  210. package/lib/langium/residency/cst-residency-service.js.map +1 -1
  211. package/lib/langium/scope/ast-node-description-provider.d.ts +46 -10
  212. package/lib/langium/scope/ast-node-description-provider.d.ts.map +1 -1
  213. package/lib/langium/scope/ast-node-description-provider.js +88 -15
  214. package/lib/langium/scope/ast-node-description-provider.js.map +1 -1
  215. package/lib/langium/scope/built-local-symbols.d.ts +12 -0
  216. package/lib/langium/scope/built-local-symbols.d.ts.map +1 -0
  217. package/lib/langium/scope/built-local-symbols.js +11 -0
  218. package/lib/langium/scope/built-local-symbols.js.map +1 -0
  219. package/lib/langium/scope/fallback-scope.d.ts +27 -0
  220. package/lib/langium/scope/fallback-scope.d.ts.map +1 -0
  221. package/lib/langium/scope/fallback-scope.js +35 -0
  222. package/lib/langium/scope/fallback-scope.js.map +1 -0
  223. package/lib/langium/scope/hydranium-scope-computation.d.ts +21 -18
  224. package/lib/langium/scope/hydranium-scope-computation.d.ts.map +1 -1
  225. package/lib/langium/scope/hydranium-scope-computation.js +38 -39
  226. package/lib/langium/scope/hydranium-scope-computation.js.map +1 -1
  227. package/lib/langium/scope/hydranium-scope-provider.d.ts +122 -32
  228. package/lib/langium/scope/hydranium-scope-provider.d.ts.map +1 -1
  229. package/lib/langium/scope/hydranium-scope-provider.js +217 -39
  230. package/lib/langium/scope/hydranium-scope-provider.js.map +1 -1
  231. package/lib/langium/scope/index.d.ts +1 -0
  232. package/lib/langium/scope/index.d.ts.map +1 -1
  233. package/lib/langium/scope/index.js +1 -0
  234. package/lib/langium/scope/index.js.map +1 -1
  235. package/lib/langium/scope/reference-builder.d.ts.map +1 -1
  236. package/lib/langium/scope/reference-builder.js.map +1 -1
  237. package/lib/langium/scope/reference-candidate-provider.d.ts +6 -5
  238. package/lib/langium/scope/reference-candidate-provider.d.ts.map +1 -1
  239. package/lib/langium/scope/reference-candidate-provider.js +3 -2
  240. package/lib/langium/scope/reference-candidate-provider.js.map +1 -1
  241. package/lib/langium/scope/scope-extension-service.d.ts +69 -37
  242. package/lib/langium/scope/scope-extension-service.d.ts.map +1 -1
  243. package/lib/langium/scope/scope-extension-service.js +102 -29
  244. package/lib/langium/scope/scope-extension-service.js.map +1 -1
  245. package/lib/langium/scope/scoped-ast-node-description.d.ts +1 -3
  246. package/lib/langium/scope/scoped-ast-node-description.d.ts.map +1 -1
  247. package/lib/langium/scope/scoped-ast-node-description.js +1 -3
  248. package/lib/langium/scope/scoped-ast-node-description.js.map +1 -1
  249. package/lib/langium/serialization/abstract-serializer.d.ts +38 -27
  250. package/lib/langium/serialization/abstract-serializer.d.ts.map +1 -1
  251. package/lib/langium/serialization/abstract-serializer.js +54 -30
  252. package/lib/langium/serialization/abstract-serializer.js.map +1 -1
  253. package/lib/langium/serialization/json-serializer.d.ts +8 -1
  254. package/lib/langium/serialization/json-serializer.d.ts.map +1 -1
  255. package/lib/langium/serialization/json-serializer.js +13 -3
  256. package/lib/langium/serialization/json-serializer.js.map +1 -1
  257. package/lib/langium/serialization/serializer.d.ts +1 -1
  258. package/lib/langium/serialization/yaml-serializer.d.ts +12 -0
  259. package/lib/langium/serialization/yaml-serializer.d.ts.map +1 -1
  260. package/lib/langium/serialization/yaml-serializer.js +24 -4
  261. package/lib/langium/serialization/yaml-serializer.js.map +1 -1
  262. package/lib/langium/service-registry.d.ts +29 -1
  263. package/lib/langium/service-registry.d.ts.map +1 -1
  264. package/lib/langium/service-registry.js +37 -0
  265. package/lib/langium/service-registry.js.map +1 -1
  266. package/lib/langium/shared-services.d.ts +10 -4
  267. package/lib/langium/shared-services.d.ts.map +1 -1
  268. package/lib/langium/shared-services.js.map +1 -1
  269. package/lib/langium/single-langium-copy.d.ts +22 -0
  270. package/lib/langium/single-langium-copy.d.ts.map +1 -0
  271. package/lib/langium/single-langium-copy.js +36 -0
  272. package/lib/langium/single-langium-copy.js.map +1 -0
  273. package/lib/langium/transfer/transfer-encoder.d.ts +60 -39
  274. package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
  275. package/lib/langium/transfer/transfer-encoder.js +35 -24
  276. package/lib/langium/transfer/transfer-encoder.js.map +1 -1
  277. package/lib/langium/trivia/comment-preserver.d.ts +543 -0
  278. package/lib/langium/trivia/comment-preserver.d.ts.map +1 -0
  279. package/lib/langium/trivia/comment-preserver.js +1165 -0
  280. package/lib/langium/trivia/comment-preserver.js.map +1 -0
  281. package/lib/langium/trivia/document-ending-preserver.d.ts +51 -0
  282. package/lib/langium/trivia/document-ending-preserver.d.ts.map +1 -0
  283. package/lib/langium/trivia/document-ending-preserver.js +62 -0
  284. package/lib/langium/trivia/document-ending-preserver.js.map +1 -0
  285. package/lib/langium/trivia/index.d.ts +14 -0
  286. package/lib/langium/trivia/index.d.ts.map +1 -0
  287. package/lib/langium/trivia/index.js +14 -0
  288. package/lib/langium/trivia/index.js.map +1 -0
  289. package/lib/langium/trivia/trivia-contribution.d.ts +37 -0
  290. package/lib/langium/trivia/trivia-contribution.d.ts.map +1 -0
  291. package/lib/langium/trivia/trivia-contribution.js +10 -0
  292. package/lib/langium/trivia/trivia-contribution.js.map +1 -0
  293. package/lib/langium/trivia/trivia-preserver.d.ts +52 -0
  294. package/lib/langium/trivia/trivia-preserver.d.ts.map +1 -0
  295. package/lib/langium/trivia/trivia-preserver.js +10 -0
  296. package/lib/langium/trivia/trivia-preserver.js.map +1 -0
  297. package/lib/langium/trivia/trivia-service.d.ts +70 -0
  298. package/lib/langium/trivia/trivia-service.d.ts.map +1 -0
  299. package/lib/langium/trivia/trivia-service.js +56 -0
  300. package/lib/langium/trivia/trivia-service.js.map +1 -0
  301. package/lib/langium/update-rewrite/normalize-empty-strings.d.ts.map +1 -1
  302. package/lib/langium/update-rewrite/update-rewrite-service.d.ts +1 -1
  303. package/lib/langium/update-rewrite/update-rewrite-service.d.ts.map +1 -1
  304. package/lib/langium/update-rewrite/update-rewrite-service.js.map +1 -1
  305. package/lib/langium/update-rewrite/update-rewrite.d.ts +1 -1
  306. package/lib/langium/validation/document-validator.d.ts +168 -23
  307. package/lib/langium/validation/document-validator.d.ts.map +1 -1
  308. package/lib/langium/validation/document-validator.js +264 -26
  309. package/lib/langium/validation/document-validator.js.map +1 -1
  310. package/lib/langium/validation/validation-contribution-collector.d.ts +9 -1
  311. package/lib/langium/validation/validation-contribution-collector.d.ts.map +1 -1
  312. package/lib/langium/validation/validation-contribution-collector.js +1 -1
  313. package/lib/langium/validation/validation-contribution-collector.js.map +1 -1
  314. package/lib/langium/workspace/ast-node-locator.d.ts +24 -0
  315. package/lib/langium/workspace/ast-node-locator.d.ts.map +1 -0
  316. package/lib/langium/workspace/ast-node-locator.js +32 -0
  317. package/lib/langium/workspace/ast-node-locator.js.map +1 -0
  318. package/lib/langium/workspace/document-uri-policy.d.ts +24 -15
  319. package/lib/langium/workspace/document-uri-policy.d.ts.map +1 -1
  320. package/lib/langium/workspace/document-uri-policy.js +22 -13
  321. package/lib/langium/workspace/document-uri-policy.js.map +1 -1
  322. package/lib/langium/workspace/file-not-found.d.ts +23 -0
  323. package/lib/langium/workspace/file-not-found.d.ts.map +1 -0
  324. package/lib/langium/workspace/file-not-found.js +31 -0
  325. package/lib/langium/workspace/file-not-found.js.map +1 -0
  326. package/lib/langium/workspace/file-system-provider.d.ts +167 -8
  327. package/lib/langium/workspace/file-system-provider.d.ts.map +1 -1
  328. package/lib/langium/workspace/file-system-provider.js +115 -13
  329. package/lib/langium/workspace/file-system-provider.js.map +1 -1
  330. package/lib/langium/workspace/hydranium-langium-document-factory.d.ts +78 -4
  331. package/lib/langium/workspace/hydranium-langium-document-factory.d.ts.map +1 -1
  332. package/lib/langium/workspace/hydranium-langium-document-factory.js +188 -5
  333. package/lib/langium/workspace/hydranium-langium-document-factory.js.map +1 -1
  334. package/lib/langium/workspace/hydranium-workspace-lock.d.ts +50 -1
  335. package/lib/langium/workspace/hydranium-workspace-lock.d.ts.map +1 -1
  336. package/lib/langium/workspace/hydranium-workspace-lock.js +73 -1
  337. package/lib/langium/workspace/hydranium-workspace-lock.js.map +1 -1
  338. package/lib/langium/workspace/hydranium-workspace-manager.d.ts +105 -21
  339. package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
  340. package/lib/langium/workspace/hydranium-workspace-manager.js +186 -31
  341. package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
  342. package/lib/langium/workspace/in-memory-file-system-provider.d.ts +26 -9
  343. package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
  344. package/lib/langium/workspace/in-memory-file-system-provider.js +45 -22
  345. package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
  346. package/lib/langium/workspace/index-manager.d.ts.map +1 -1
  347. package/lib/langium/workspace/index-manager.js.map +1 -1
  348. package/lib/langium/workspace/index.d.ts +3 -0
  349. package/lib/langium/workspace/index.d.ts.map +1 -1
  350. package/lib/langium/workspace/index.js +3 -0
  351. package/lib/langium/workspace/index.js.map +1 -1
  352. package/lib/langium/workspace/initialize-workspace.d.ts +40 -3
  353. package/lib/langium/workspace/initialize-workspace.d.ts.map +1 -1
  354. package/lib/langium/workspace/initialize-workspace.js +68 -3
  355. package/lib/langium/workspace/initialize-workspace.js.map +1 -1
  356. package/lib/langium/workspace/langium-documents.d.ts +7 -0
  357. package/lib/langium/workspace/langium-documents.d.ts.map +1 -1
  358. package/lib/langium/workspace/langium-documents.js +18 -1
  359. package/lib/langium/workspace/langium-documents.js.map +1 -1
  360. package/lib/langium/workspace/persistent-file-system-provider.d.ts +16 -10
  361. package/lib/langium/workspace/persistent-file-system-provider.d.ts.map +1 -1
  362. package/lib/langium/workspace/persistent-file-system-provider.js +25 -13
  363. package/lib/langium/workspace/persistent-file-system-provider.js.map +1 -1
  364. package/lib/{documents → langium/workspace}/self-save-registry.d.ts +7 -9
  365. package/lib/langium/workspace/self-save-registry.d.ts.map +1 -0
  366. package/lib/{documents → langium/workspace}/self-save-registry.js +9 -11
  367. package/lib/langium/workspace/self-save-registry.js.map +1 -0
  368. package/lib/langium/workspace/syntax-errors.d.ts +23 -0
  369. package/lib/langium/workspace/syntax-errors.d.ts.map +1 -0
  370. package/lib/langium/workspace/syntax-errors.js +29 -0
  371. package/lib/langium/workspace/syntax-errors.js.map +1 -0
  372. package/lib/langium/workspace/virtual-document.d.ts +55 -21
  373. package/lib/langium/workspace/virtual-document.d.ts.map +1 -1
  374. package/lib/langium/workspace/virtual-document.js +87 -24
  375. package/lib/langium/workspace/virtual-document.js.map +1 -1
  376. package/lib/langium/workspace/write-lock-scope.d.ts +22 -10
  377. package/lib/langium/workspace/write-lock-scope.d.ts.map +1 -1
  378. package/lib/langium/workspace/write-lock-scope.js +15 -5
  379. package/lib/langium/workspace/write-lock-scope.js.map +1 -1
  380. package/lib/launcher/integrated-server.d.ts +3 -1
  381. package/lib/launcher/integrated-server.d.ts.map +1 -1
  382. package/lib/locale/server-locale.d.ts +6 -6
  383. package/lib/locale/server-locale.d.ts.map +1 -1
  384. package/lib/locale/server-locale.js.map +1 -1
  385. package/lib/lsp/completion/hydranium-completion-provider.d.ts +44 -2
  386. package/lib/lsp/completion/hydranium-completion-provider.d.ts.map +1 -1
  387. package/lib/lsp/completion/hydranium-completion-provider.js +68 -0
  388. package/lib/lsp/completion/hydranium-completion-provider.js.map +1 -1
  389. package/lib/lsp/connection-features.d.ts +23 -0
  390. package/lib/lsp/connection-features.d.ts.map +1 -0
  391. package/lib/lsp/connection-features.js +60 -0
  392. package/lib/lsp/connection-features.js.map +1 -0
  393. package/lib/lsp/diagnostics-connection.d.ts +27 -0
  394. package/lib/lsp/diagnostics-connection.d.ts.map +1 -0
  395. package/lib/lsp/diagnostics-connection.js +53 -0
  396. package/lib/lsp/diagnostics-connection.js.map +1 -0
  397. package/lib/lsp/edit-requests-guard.d.ts +31 -0
  398. package/lib/lsp/edit-requests-guard.d.ts.map +1 -0
  399. package/lib/lsp/edit-requests-guard.js +125 -0
  400. package/lib/lsp/edit-requests-guard.js.map +1 -0
  401. package/lib/lsp/hydranium-document-update-handler.d.ts +126 -82
  402. package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
  403. package/lib/lsp/hydranium-document-update-handler.js +234 -101
  404. package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
  405. package/lib/lsp/index.d.ts +6 -2
  406. package/lib/lsp/index.d.ts.map +1 -1
  407. package/lib/lsp/index.js +6 -2
  408. package/lib/lsp/index.js.map +1 -1
  409. package/lib/lsp/lsp-latency.d.ts +39 -0
  410. package/lib/lsp/lsp-latency.d.ts.map +1 -0
  411. package/lib/lsp/lsp-latency.js +58 -0
  412. package/lib/lsp/lsp-latency.js.map +1 -0
  413. package/lib/lsp/save-text-document.d.ts +31 -0
  414. package/lib/lsp/save-text-document.d.ts.map +1 -0
  415. package/lib/lsp/save-text-document.js +52 -0
  416. package/lib/lsp/save-text-document.js.map +1 -0
  417. package/lib/lsp/semantic-token-provider.d.ts +58 -2
  418. package/lib/lsp/semantic-token-provider.d.ts.map +1 -1
  419. package/lib/lsp/semantic-token-provider.js +98 -9
  420. package/lib/lsp/semantic-token-provider.js.map +1 -1
  421. package/lib/lsp/shared-module.d.ts +4 -8
  422. package/lib/lsp/shared-module.d.ts.map +1 -1
  423. package/lib/lsp/shared-module.js.map +1 -1
  424. package/lib/lsp/start-language-server.d.ts +12 -7
  425. package/lib/lsp/start-language-server.d.ts.map +1 -1
  426. package/lib/lsp/start-language-server.js +21 -7
  427. package/lib/lsp/start-language-server.js.map +1 -1
  428. package/lib/messages/carriers.d.ts +4 -6
  429. package/lib/messages/carriers.d.ts.map +1 -1
  430. package/lib/messages/carriers.js +4 -6
  431. package/lib/messages/carriers.js.map +1 -1
  432. package/lib/messages/index.d.ts +2 -2
  433. package/lib/messages/index.d.ts.map +1 -1
  434. package/lib/messages/index.js +2 -2
  435. package/lib/messages/index.js.map +1 -1
  436. package/lib/messages/renderer.d.ts +10 -4
  437. package/lib/messages/renderer.d.ts.map +1 -1
  438. package/lib/messages/renderer.js +6 -0
  439. package/lib/messages/renderer.js.map +1 -1
  440. package/lib/node/event-loop-monitor.d.ts.map +1 -1
  441. package/lib/node/event-loop-monitor.js +3 -2
  442. package/lib/node/event-loop-monitor.js.map +1 -1
  443. package/lib/node/heap-ceiling.d.ts +61 -0
  444. package/lib/node/heap-ceiling.d.ts.map +1 -0
  445. package/lib/node/heap-ceiling.js +72 -0
  446. package/lib/node/heap-ceiling.js.map +1 -0
  447. package/lib/node/index.d.ts +1 -1
  448. package/lib/node/index.d.ts.map +1 -1
  449. package/lib/node/index.js +5 -1
  450. package/lib/node/index.js.map +1 -1
  451. package/lib/node/latency-from-env.d.ts +1 -1
  452. package/lib/node/latency-from-env.js +1 -1
  453. package/lib/node/lint-grammar.js.map +1 -1
  454. package/lib/node/log-file-sink.d.ts +3 -2
  455. package/lib/node/log-file-sink.d.ts.map +1 -1
  456. package/lib/node/log-file-sink.js +6 -2
  457. package/lib/node/log-file-sink.js.map +1 -1
  458. package/lib/node/log-preamble-node.d.ts +5 -3
  459. package/lib/node/log-preamble-node.d.ts.map +1 -1
  460. package/lib/node/log-preamble-node.js +13 -10
  461. package/lib/node/log-preamble-node.js.map +1 -1
  462. package/lib/node/measure-memory.d.ts.map +1 -1
  463. package/lib/node/measure-memory.js +5 -4
  464. package/lib/node/measure-memory.js.map +1 -1
  465. package/lib/node/memory-monitor.d.ts +2 -2
  466. package/lib/node/memory-monitor.d.ts.map +1 -1
  467. package/lib/node/memory-monitor.js +9 -10
  468. package/lib/node/memory-monitor.js.map +1 -1
  469. package/lib/node/node-file-system-provider.d.ts +67 -9
  470. package/lib/node/node-file-system-provider.d.ts.map +1 -1
  471. package/lib/node/node-file-system-provider.js +198 -53
  472. package/lib/node/node-file-system-provider.js.map +1 -1
  473. package/lib/node/profile-capture.d.ts +12 -4
  474. package/lib/node/profile-capture.d.ts.map +1 -1
  475. package/lib/node/profile-capture.js +8 -6
  476. package/lib/node/profile-capture.js.map +1 -1
  477. package/lib/node/profile-digest.js.map +1 -1
  478. package/lib/node/profiling-run.d.ts +13 -3
  479. package/lib/node/profiling-run.d.ts.map +1 -1
  480. package/lib/node/profiling-run.js +17 -8
  481. package/lib/node/profiling-run.js.map +1 -1
  482. package/lib/node/rename-over-open-readers.d.ts +61 -0
  483. package/lib/node/rename-over-open-readers.d.ts.map +1 -0
  484. package/lib/node/rename-over-open-readers.js +80 -0
  485. package/lib/node/rename-over-open-readers.js.map +1 -0
  486. package/lib/node/server-diagnostics.js +1 -1
  487. package/lib/node/server-diagnostics.js.map +1 -1
  488. package/lib/node/server-state-snapshot.d.ts +6 -5
  489. package/lib/node/server-state-snapshot.d.ts.map +1 -1
  490. package/lib/node/server-state-snapshot.js +20 -17
  491. package/lib/node/server-state-snapshot.js.map +1 -1
  492. package/lib/node/socket-launcher.d.ts +43 -7
  493. package/lib/node/socket-launcher.d.ts.map +1 -1
  494. package/lib/node/socket-launcher.js +42 -7
  495. package/lib/node/socket-launcher.js.map +1 -1
  496. package/lib/node/startup-cpu-profile.d.ts +29 -0
  497. package/lib/node/startup-cpu-profile.d.ts.map +1 -0
  498. package/lib/node/startup-cpu-profile.js +91 -0
  499. package/lib/node/startup-cpu-profile.js.map +1 -0
  500. package/lib/node/stdio-launcher.d.ts +13 -13
  501. package/lib/node/stdio-launcher.d.ts.map +1 -1
  502. package/lib/node/stdio-launcher.js +21 -11
  503. package/lib/node/stdio-launcher.js.map +1 -1
  504. package/lib/node/validate-workspace.d.ts +1 -1
  505. package/lib/node/validate-workspace.d.ts.map +1 -1
  506. package/lib/node/validate-workspace.js +5 -1
  507. package/lib/node/validate-workspace.js.map +1 -1
  508. package/lib/testing/document-uri-policy-conformance.js +1 -1
  509. package/lib/testing/document-uri-policy-conformance.js.map +1 -1
  510. package/lib/testing/fake-description.js.map +1 -1
  511. package/lib/testing/fake-document.d.ts +2 -1
  512. package/lib/testing/fake-document.d.ts.map +1 -1
  513. package/lib/testing/fake-document.js.map +1 -1
  514. package/lib/testing/index.d.ts +1 -1
  515. package/lib/testing/index.d.ts.map +1 -1
  516. package/lib/testing/index.js +2 -1
  517. package/lib/testing/index.js.map +1 -1
  518. package/lib/testing/langium-test-helpers.d.ts.map +1 -1
  519. package/lib/testing/langium-test-helpers.js +1 -0
  520. package/lib/testing/langium-test-helpers.js.map +1 -1
  521. package/lib/testing/make-noop-language-services.js.map +1 -1
  522. package/lib/testing/make-noop-shared-services.js.map +1 -1
  523. package/lib/testing/make-test-services.d.ts +45 -14
  524. package/lib/testing/make-test-services.d.ts.map +1 -1
  525. package/lib/testing/make-test-services.js +23 -7
  526. package/lib/testing/make-test-services.js.map +1 -1
  527. package/lib/testing/make-test-tracer.d.ts +12 -0
  528. package/lib/testing/make-test-tracer.d.ts.map +1 -1
  529. package/lib/testing/make-test-tracer.js +13 -0
  530. package/lib/testing/make-test-tracer.js.map +1 -1
  531. package/lib/testing/node/applied-edit-capture.d.ts +29 -0
  532. package/lib/testing/node/applied-edit-capture.d.ts.map +1 -0
  533. package/lib/testing/node/applied-edit-capture.js +117 -0
  534. package/lib/testing/node/applied-edit-capture.js.map +1 -0
  535. package/lib/testing/node/golden-corpus.js.map +1 -1
  536. package/lib/testing/node/index.d.ts +4 -1
  537. package/lib/testing/node/index.d.ts.map +1 -1
  538. package/lib/testing/node/index.js +4 -1
  539. package/lib/testing/node/index.js.map +1 -1
  540. package/lib/testing/node/lsp-harness.d.ts.map +1 -1
  541. package/lib/testing/node/lsp-harness.js +2 -0
  542. package/lib/testing/node/lsp-harness.js.map +1 -1
  543. package/lib/testing/node/lsp-server-connection.d.ts +21 -6
  544. package/lib/testing/node/lsp-server-connection.d.ts.map +1 -1
  545. package/lib/testing/node/lsp-server-connection.js +49 -106
  546. package/lib/testing/node/lsp-server-connection.js.map +1 -1
  547. package/lib/testing/node/scratch-workspace.d.ts +2 -1
  548. package/lib/testing/node/scratch-workspace.d.ts.map +1 -1
  549. package/lib/testing/node/scratch-workspace.js +12 -6
  550. package/lib/testing/node/scratch-workspace.js.map +1 -1
  551. package/lib/testing/node/settle-server.d.ts +33 -0
  552. package/lib/testing/node/settle-server.d.ts.map +1 -0
  553. package/lib/testing/node/settle-server.js +84 -0
  554. package/lib/testing/node/settle-server.js.map +1 -0
  555. package/lib/testing/node/spawned-server.d.ts +30 -1
  556. package/lib/testing/node/spawned-server.d.ts.map +1 -1
  557. package/lib/testing/node/spawned-server.js +39 -2
  558. package/lib/testing/node/spawned-server.js.map +1 -1
  559. package/lib/testing/playwright/browser-capture-bridge.js.map +1 -1
  560. package/lib/testing/playwright/e2e-profiling.js.map +1 -1
  561. package/lib/testing/playwright/flaky-network-proxy.d.ts +76 -0
  562. package/lib/testing/playwright/flaky-network-proxy.d.ts.map +1 -0
  563. package/lib/testing/playwright/flaky-network-proxy.js +164 -0
  564. package/lib/testing/playwright/flaky-network-proxy.js.map +1 -0
  565. package/lib/testing/playwright/index.d.ts +1 -0
  566. package/lib/testing/playwright/index.d.ts.map +1 -1
  567. package/lib/testing/playwright/index.js +1 -0
  568. package/lib/testing/playwright/index.js.map +1 -1
  569. package/lib/testing/playwright/server-log-capture.d.ts +46 -16
  570. package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
  571. package/lib/testing/playwright/server-log-capture.js +111 -40
  572. package/lib/testing/playwright/server-log-capture.js.map +1 -1
  573. package/lib/testing/playwright/server-log-fixture.d.ts +16 -0
  574. package/lib/testing/playwright/server-log-fixture.d.ts.map +1 -1
  575. package/lib/testing/playwright/server-log-fixture.js +25 -6
  576. package/lib/testing/playwright/server-log-fixture.js.map +1 -1
  577. package/lib/testing/playwright/server-log-region.d.ts +15 -0
  578. package/lib/testing/playwright/server-log-region.d.ts.map +1 -0
  579. package/lib/testing/playwright/server-log-region.js +30 -0
  580. package/lib/testing/playwright/server-log-region.js.map +1 -0
  581. package/lib/testing/playwright/server-log-rename-reporter.d.ts.map +1 -1
  582. package/lib/testing/playwright/server-log-rename-reporter.js +4 -3
  583. package/lib/testing/playwright/server-log-rename-reporter.js.map +1 -1
  584. package/lib/testing/run-update-pipeline.d.ts +8 -5
  585. package/lib/testing/run-update-pipeline.d.ts.map +1 -1
  586. package/lib/testing/run-update-pipeline.js.map +1 -1
  587. package/lib/testing/stub-ast-document-manager.d.ts +11 -51
  588. package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
  589. package/lib/testing/stub-ast-document-manager.js +31 -59
  590. package/lib/testing/stub-ast-document-manager.js.map +1 -1
  591. package/lib/testing/stub-document-builder.d.ts +25 -11
  592. package/lib/testing/stub-document-builder.d.ts.map +1 -1
  593. package/lib/testing/stub-document-builder.js +91 -2
  594. package/lib/testing/stub-document-builder.js.map +1 -1
  595. package/lib/testing/stub-hydranium-text-documents.d.ts +33 -10
  596. package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
  597. package/lib/testing/stub-hydranium-text-documents.js +132 -16
  598. package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
  599. package/lib/testing/stub-index-manager.js.map +1 -1
  600. package/lib/testing/stub-langium-documents.d.ts +3 -2
  601. package/lib/testing/stub-langium-documents.d.ts.map +1 -1
  602. package/lib/testing/stub-langium-documents.js.map +1 -1
  603. package/lib/testing/stub-model-service.d.ts +8 -7
  604. package/lib/testing/stub-model-service.d.ts.map +1 -1
  605. package/lib/testing/stub-model-service.js +4 -4
  606. package/lib/testing/stub-model-service.js.map +1 -1
  607. package/lib/testing/stub-project-manager.d.ts.map +1 -1
  608. package/lib/testing/stub-project-manager.js +10 -0
  609. package/lib/testing/stub-project-manager.js.map +1 -1
  610. package/lib/testing/stub-self-save-registry.d.ts +1 -1
  611. package/lib/testing/stub-self-save-registry.d.ts.map +1 -1
  612. package/lib/testing/stub-service-registry.d.ts +1 -1
  613. package/lib/testing/stub-service-registry.js.map +1 -1
  614. package/lib/testing/stub-writable-file-system.d.ts +1 -1
  615. package/lib/testing/stub-writable-file-system.d.ts.map +1 -1
  616. package/lib/testing/stub-writable-file-system.js +2 -1
  617. package/lib/testing/stub-writable-file-system.js.map +1 -1
  618. package/lib/util/connection-liveness.d.ts +22 -6
  619. package/lib/util/connection-liveness.d.ts.map +1 -1
  620. package/lib/util/connection-liveness.js +75 -15
  621. package/lib/util/connection-liveness.js.map +1 -1
  622. package/lib/util/environment.d.ts +7 -0
  623. package/lib/util/environment.d.ts.map +1 -1
  624. package/lib/util/environment.js +15 -1
  625. package/lib/util/environment.js.map +1 -1
  626. package/lib/util/registry.d.ts.map +1 -1
  627. package/package.json +41 -60
  628. package/src/browser/host-heads-in-worker.ts +218 -0
  629. package/src/browser/index.ts +17 -0
  630. package/src/browser/indexeddb-file-system-store.ts +192 -0
  631. package/src/browser/start-language-server-in-worker.ts +56 -0
  632. package/src/documents/ast-document-manager.ts +477 -250
  633. package/src/documents/client-ids.ts +5 -22
  634. package/src/documents/client-session-errors.ts +26 -0
  635. package/src/documents/client-session-registry.ts +262 -0
  636. package/src/documents/dirty-state-tracker.ts +167 -0
  637. package/src/documents/document-release-handler.ts +404 -0
  638. package/src/documents/document-release-scheduler.ts +121 -0
  639. package/src/documents/file-system-task-queue.ts +77 -0
  640. package/src/documents/hydranium-text-documents.ts +1044 -615
  641. package/src/documents/index.ts +10 -2
  642. package/src/documents/language-client-shadow.ts +501 -0
  643. package/src/documents/model-ledger.ts +66 -0
  644. package/src/documents/text-ledger.ts +112 -0
  645. package/src/documents/version-sync-service.ts +304 -0
  646. package/src/index.ts +3 -0
  647. package/src/langium/ast-extension/ast-extension-service.ts +77 -41
  648. package/src/langium/ast-extension/ast-node-builder.ts +35 -13
  649. package/src/langium/bootstrap.ts +79 -43
  650. package/src/langium/build-phase-pass/build-phase-pass-service.ts +29 -19
  651. package/src/langium/build-phase-pass/build-phase-pass.ts +37 -8
  652. package/src/langium/composite-ast-reflection.ts +112 -0
  653. package/src/langium/config/configuration-provider.ts +119 -0
  654. package/src/langium/config/index.ts +1 -0
  655. package/src/langium/diagnostics/console-tee.ts +150 -0
  656. package/src/langium/diagnostics/hydranium-langium-profiler.ts +15 -39
  657. package/src/langium/diagnostics/log-preamble.ts +1 -1
  658. package/src/langium/diagnostics/logger.ts +51 -17
  659. package/src/langium/diagnostics/lsp-logger.ts +83 -45
  660. package/src/langium/document-builder/build-pipeline-integration.ts +57 -32
  661. package/src/langium/document-builder/build-session.ts +4 -3
  662. package/src/langium/document-builder/document-builder.ts +773 -106
  663. package/src/langium/integration-services.ts +20 -23
  664. package/src/langium/integrity/integrity-rule.ts +22 -8
  665. package/src/langium/integrity/integrity-service.ts +656 -90
  666. package/src/langium/keys/containment.ts +3 -2
  667. package/src/langium/keys/name-based-key-provider.ts +24 -12
  668. package/src/langium/language-module.ts +52 -9
  669. package/src/langium/model-service/client-session.ts +769 -0
  670. package/src/langium/model-service/index.ts +2 -0
  671. package/src/langium/model-service/model-events.ts +82 -0
  672. package/src/langium/model-service/model-service.ts +885 -532
  673. package/src/langium/module.ts +141 -65
  674. package/src/langium/naming/name-provider.ts +34 -8
  675. package/src/langium/project/abstract-project-manager.ts +409 -20
  676. package/src/langium/project/project-change-event.ts +25 -3
  677. package/src/langium/project/project-manager.ts +31 -2
  678. package/src/langium/project/single-project-manager.ts +16 -1
  679. package/src/langium/residency/cst-residency-service.ts +210 -87
  680. package/src/langium/scope/ast-node-description-provider.ts +111 -16
  681. package/src/langium/scope/built-local-symbols.ts +13 -0
  682. package/src/langium/scope/fallback-scope.ts +38 -0
  683. package/src/langium/scope/hydranium-scope-computation.ts +45 -44
  684. package/src/langium/scope/hydranium-scope-provider.ts +245 -41
  685. package/src/langium/scope/index.ts +1 -0
  686. package/src/langium/scope/reference-candidate-provider.ts +6 -5
  687. package/src/langium/scope/scope-extension-service.ts +148 -56
  688. package/src/langium/scope/scoped-ast-node-description.ts +1 -3
  689. package/src/langium/serialization/abstract-serializer.ts +57 -41
  690. package/src/langium/serialization/json-serializer.ts +14 -3
  691. package/src/langium/serialization/serializer.ts +1 -1
  692. package/src/langium/serialization/yaml-serializer.ts +26 -4
  693. package/src/langium/service-registry.ts +41 -2
  694. package/src/langium/shared-services.ts +13 -4
  695. package/src/langium/single-langium-copy.ts +42 -0
  696. package/src/langium/transfer/transfer-encoder.ts +76 -50
  697. package/src/langium/trivia/comment-preserver.ts +1410 -0
  698. package/src/langium/trivia/document-ending-preserver.ts +71 -0
  699. package/src/langium/trivia/index.ts +14 -0
  700. package/src/langium/trivia/trivia-contribution.ts +39 -0
  701. package/src/langium/trivia/trivia-preserver.ts +56 -0
  702. package/src/langium/trivia/trivia-service.ts +99 -0
  703. package/src/langium/update-rewrite/update-rewrite-service.ts +1 -1
  704. package/src/langium/update-rewrite/update-rewrite.ts +1 -1
  705. package/src/langium/validation/document-validator.ts +312 -31
  706. package/src/langium/validation/validation-contribution-collector.ts +10 -1
  707. package/src/langium/workspace/ast-node-locator.ts +33 -0
  708. package/src/langium/workspace/document-uri-policy.ts +32 -18
  709. package/src/langium/workspace/file-not-found.ts +35 -0
  710. package/src/langium/workspace/file-system-provider.ts +258 -16
  711. package/src/langium/workspace/hydranium-langium-document-factory.ts +239 -4
  712. package/src/langium/workspace/hydranium-workspace-lock.ts +101 -2
  713. package/src/langium/workspace/hydranium-workspace-manager.ts +214 -35
  714. package/src/langium/workspace/in-memory-file-system-provider.ts +54 -23
  715. package/src/langium/workspace/index.ts +3 -0
  716. package/src/langium/workspace/initialize-workspace.ts +96 -4
  717. package/src/langium/workspace/langium-documents.ts +19 -1
  718. package/src/langium/workspace/persistent-file-system-provider.ts +27 -15
  719. package/src/{documents → langium/workspace}/self-save-registry.ts +12 -14
  720. package/src/langium/workspace/syntax-errors.ts +34 -0
  721. package/src/langium/workspace/virtual-document.ts +106 -25
  722. package/src/langium/workspace/write-lock-scope.ts +27 -12
  723. package/src/launcher/integrated-server.ts +3 -1
  724. package/src/locale/server-locale.ts +6 -6
  725. package/src/lsp/completion/hydranium-completion-provider.ts +82 -2
  726. package/src/lsp/connection-features.ts +67 -0
  727. package/src/lsp/diagnostics-connection.ts +56 -0
  728. package/src/lsp/edit-requests-guard.ts +152 -0
  729. package/src/lsp/hydranium-document-update-handler.ts +284 -110
  730. package/src/lsp/index.ts +6 -2
  731. package/src/lsp/lsp-latency.ts +62 -0
  732. package/src/lsp/save-text-document.ts +63 -0
  733. package/src/lsp/semantic-token-provider.ts +146 -8
  734. package/src/lsp/shared-module.ts +4 -8
  735. package/src/lsp/start-language-server.ts +21 -7
  736. package/src/messages/carriers.ts +4 -6
  737. package/src/messages/index.ts +11 -2
  738. package/src/messages/renderer.ts +11 -3
  739. package/src/node/event-loop-monitor.ts +3 -4
  740. package/src/node/heap-ceiling.ts +108 -0
  741. package/src/node/index.ts +5 -1
  742. package/src/node/latency-from-env.ts +1 -1
  743. package/src/node/log-file-sink.ts +6 -2
  744. package/src/node/log-preamble-node.ts +20 -10
  745. package/src/node/measure-memory.ts +5 -4
  746. package/src/node/memory-monitor.ts +10 -10
  747. package/src/node/node-file-system-provider.ts +214 -57
  748. package/src/node/profile-capture.ts +16 -15
  749. package/src/node/profiling-run.ts +30 -8
  750. package/src/node/rename-over-open-readers.ts +94 -0
  751. package/src/node/server-diagnostics.ts +1 -1
  752. package/src/node/server-state-snapshot.ts +28 -20
  753. package/src/node/socket-launcher.ts +78 -14
  754. package/src/node/startup-cpu-profile.ts +94 -0
  755. package/src/node/stdio-launcher.ts +29 -19
  756. package/src/node/validate-workspace.ts +8 -2
  757. package/src/testing/document-uri-policy-conformance.ts +1 -1
  758. package/src/testing/fake-document.ts +2 -1
  759. package/src/testing/index.ts +2 -1
  760. package/src/testing/langium-test-helpers.ts +1 -0
  761. package/src/testing/make-test-services.ts +83 -31
  762. package/src/testing/make-test-tracer.ts +26 -0
  763. package/src/testing/node/applied-edit-capture.ts +141 -0
  764. package/src/testing/node/index.ts +4 -1
  765. package/src/testing/node/lsp-harness.ts +2 -0
  766. package/src/testing/node/lsp-server-connection.ts +83 -119
  767. package/src/testing/node/scratch-workspace.ts +12 -6
  768. package/src/testing/node/settle-server.ts +89 -0
  769. package/src/testing/node/spawned-server.ts +57 -3
  770. package/src/testing/parse-semantic-root.ts +1 -1
  771. package/src/testing/playwright/flaky-network-proxy.ts +240 -0
  772. package/src/testing/playwright/index.ts +1 -0
  773. package/src/testing/playwright/server-log-capture.ts +137 -47
  774. package/src/testing/playwright/server-log-fixture.ts +46 -6
  775. package/src/testing/playwright/server-log-region.ts +32 -0
  776. package/src/testing/playwright/server-log-rename-reporter.ts +4 -3
  777. package/src/testing/run-update-pipeline.ts +8 -5
  778. package/src/testing/stub-ast-document-manager.ts +47 -123
  779. package/src/testing/stub-document-builder.ts +128 -15
  780. package/src/testing/stub-hydranium-text-documents.ts +184 -27
  781. package/src/testing/stub-langium-documents.ts +3 -2
  782. package/src/testing/stub-model-service.ts +9 -8
  783. package/src/testing/stub-project-manager.ts +10 -0
  784. package/src/testing/stub-self-save-registry.ts +1 -1
  785. package/src/testing/stub-service-registry.ts +1 -1
  786. package/src/testing/stub-writable-file-system.ts +3 -2
  787. package/src/util/connection-liveness.ts +81 -15
  788. package/src/util/environment.ts +24 -2
  789. package/lib/documents/language-client-text-shadow.d.ts +0 -124
  790. package/lib/documents/language-client-text-shadow.d.ts.map +0 -1
  791. package/lib/documents/language-client-text-shadow.js +0 -190
  792. package/lib/documents/language-client-text-shadow.js.map +0 -1
  793. package/lib/documents/self-save-registry.d.ts.map +0 -1
  794. package/lib/documents/self-save-registry.js.map +0 -1
  795. package/lib/lsp/instrument-connection.d.ts +0 -36
  796. package/lib/lsp/instrument-connection.d.ts.map +0 -1
  797. package/lib/lsp/instrument-connection.js +0 -62
  798. package/lib/lsp/instrument-connection.js.map +0 -1
  799. package/lib/node/process-memory.d.ts +0 -66
  800. package/lib/node/process-memory.d.ts.map +0 -1
  801. package/lib/node/process-memory.js +0 -252
  802. package/lib/node/process-memory.js.map +0 -1
  803. package/src/documents/language-client-text-shadow.ts +0 -211
  804. package/src/lsp/instrument-connection.ts +0 -66
  805. package/src/node/process-memory.ts +0 -299
@@ -19,11 +19,17 @@
19
19
  // eslint-disable-next-line @typescript-eslint/no-restricted-imports
20
20
  import { NormalizedTextDocuments } from '@hydranium/langium/lsp';
21
21
  import { UriUtils } from '@hydranium/langium';
22
- import { OptionalVersionedTextDocumentIdentifier, TextDocumentEdit, TextDocumentSyncKind } from 'vscode-languageserver';
22
+ import { Emitter, OptionalVersionedTextDocumentIdentifier, TextDocumentEdit, TextDocumentSyncKind } from 'vscode-languageserver';
23
23
  import { TextDocument } from 'vscode-languageserver-textdocument';
24
- import { asLanguageClientUri, DisposableCollection } from '@hydranium/protocol';
24
+ import { asLanguageClientUri, asModelVersion, ConflictError, DisposableCollection, DocumentNotOpenError, STALE_BASE_VERSION_SAVE, STALE_VERSION, UNRECORDED_VERSION, describeError, describeErrorWithStack } from '@hydranium/protocol';
25
25
  import { LANGUAGE_CLIENT_ID } from './client-ids.js';
26
- import { isFullReplace, LanguageClientTextShadow } from './language-client-text-shadow.js';
26
+ import { INTEGRITY_CLIENT_ID } from '../langium/integrity/integrity-rule.js';
27
+ import { ClientSessionRegistry } from './client-session-registry.js';
28
+ import { DefaultLanguageClientShadow } from './language-client-shadow.js';
29
+ import { DefaultTextLedger } from './text-ledger.js';
30
+ import { isDocumentReleaseSkippedError } from './document-release-handler.js';
31
+ import { DefaultDocumentReleaseScheduler } from './document-release-scheduler.js';
32
+ import { DefaultDirtyStateTracker } from './dirty-state-tracker.js';
27
33
  /**
28
34
  * The LSP spec's "version is intentionally unknown" for an
29
35
  * {@link OptionalVersionedTextDocumentIdentifier} is `null`, but
@@ -33,141 +39,126 @@ import { isFullReplace, LanguageClientTextShadow } from './language-client-text-
33
39
  */
34
40
  const UNKNOWN_CLIENT_VERSION = null;
35
41
  /**
36
- * Upper bound on {@link HydraniumTextDocuments.__pendingPushes} entries
37
- * per URI. Echoes normally return within milliseconds and consume their
38
- * entry; a queue this deep means the client stopped echoing — cap instead
39
- * of leaking.
42
+ * The default of {@link HydraniumTextDocumentsOptions.releaseGraceMs}: long
43
+ * enough for a client whose connection dropped to register again and reopen
44
+ * its documents, which a data client does as soon as it has a connection.
40
45
  */
41
- const PENDING_ECHO_CAP = 32;
46
+ const DEFAULT_RELEASE_GRACE_MS = 10_000;
42
47
  /**
43
- * Cheap, stable, non-cryptographic content hash (cyrb53) for the version
44
- * sequence's "did the content change across close/reopen?" question. Only
45
- * needs to be collision-resistant enough that an accidental match across two
46
- * DIFFERENT revisions of the same file is practically impossible; 53 bits of
47
- * a well-mixed hash over full text + length gives that without pulling in
48
- * `node:crypto` (this module must stay runnable in browser hosts).
49
- */
50
- function contentHash(text) {
51
- let h1 = 0xdeadbeef;
52
- let h2 = 0x41c6ce57;
53
- for (let index = 0; index < text.length; index++) {
54
- const code = text.charCodeAt(index);
55
- h1 = Math.imul(h1 ^ code, 2654435761);
56
- h2 = Math.imul(h2 ^ code, 1597334677);
57
- }
58
- h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
59
- h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
60
- h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
61
- h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
62
- return `${(4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36)}:${text.length}`;
63
- }
64
- /**
65
- * Multi-client text-document tracking on top of Langium's `NormalizedTextDocuments`.
48
+ * The one text store every head writes to, on top of Langium's
49
+ * `NormalizedTextDocuments`, and the LSP text-sync endpoint: every
50
+ * `textDocument/*` notification and every `workspace/applyEdit` push goes
51
+ * through here.
52
+ *
53
+ * Each open, change, close and save is one synchronous transition over the
54
+ * held document and the collaborators its `create…` methods build. A
55
+ * collaborator that defers its part lets a listener of the transition's event
56
+ * read state from before it. A document no client holds any more is
57
+ * released to the `DocumentReleaseHandler` slot, after
58
+ * {@link HydraniumTextDocumentsOptions.releaseGraceMs} when its last client's
59
+ * connection was lost.
66
60
  *
67
- * Adds the framework features used by the integrity, model-server, and GLSP layers:
68
- * - Per-document client membership (multiple clients can attach to the same URI).
69
- * - A SERVER-OWNED shared version sequence: per-URI, monotonic across
70
- * close/reopen cycles, advancing exactly when the synced content changes.
71
- * Client-declared version ids (Monaco's buffer numbering) feed only a
72
- * per-client staleness guard and never leak into the shared sequence —
73
- * the two are different things (an editor's edit-operation counter vs the
74
- * document's content-revision number), and splicing them lets versions drift
75
- * silently past `baseVersion` gate holders.
76
- * - Version-author history so each edit is attributable to its originating client.
77
- * - Pending-content staging used by the integrity service to thread corrections
78
- * through `workspace/applyEdit` cycles for currently-closed documents.
79
- * - `didOpen` notifications arriving over the LSP connection wait on the
80
- * workspace-ready promise, so a client's first open cannot race workspace
81
- * discovery. Direct {@link notifyDidOpenTextDocument} calls (the non-LSP
82
- * heads) do not pass that gate — their caller owns the ordering.
61
+ * Client-declared version ids feed only the per-client staleness guard and
62
+ * never a running shared sequence: the two count different things, and
63
+ * splicing them lets versions drift past base-version gate holders. A URI
64
+ * with no ledger record and no built root starts at its opener's declared id:
65
+ * no version was handed out for it.
66
+ *
67
+ * `didOpen` notifications arriving over the LSP connection wait on the
68
+ * workspace-ready promise, so a client's first open cannot race workspace
69
+ * discovery. Direct {@link notifyDidOpenTextDocument} calls (the non-LSP
70
+ * heads) do not pass that gate — their caller owns the ordering.
83
71
  */
84
72
  export class HydraniumTextDocuments extends NormalizedTextDocuments {
85
73
  services;
74
+ options;
75
+ /** Content staged by integrity rules for a document no client holds, consumed by its first open. */
76
+ __pendingContent = new Map();
86
77
  /**
87
- * Per-URI client-facing tracking ({@link DocumentTracking}): client
88
- * membership, version-author history, and integrity-staged pending content,
89
- * keyed by normalized URI. One record per URI so the last-client close clears
90
- * every axis atomically. The language-client diff baseline stays in
91
- * {@link __shadow} (its own abstraction); the parsed-document store stays in
92
- * the inherited `__syncedDocuments`.
93
- */
94
- __documents = new Map();
95
- /**
96
- * Per-URI shared-version continuity across close/reopen cycles
97
- * ({@link VersionSequence}), written at last-client close and consulted by
98
- * the next first-client open. DELIBERATELY outside {@link DocumentTracking}:
99
- * that record is deleted on last close, while the version sequence must
100
- * survive it — the shared version is a server-owned, monotonic,
101
- * advances-iff-content-changes counter that never resets while the server
102
- * lives. That invariant is what makes an optimistic `baseVersion` gate
103
- * sound: "version unchanged ⇔ content unchanged", with no false conflicts
104
- * from close/reopen version resets and no false passes from a reopened
105
- * sequence coincidentally landing on a stale writer's number.
106
- *
107
- * Never pruned: two small values per URI ever touched — bounded by
108
- * workspace size, not by activity.
109
- */
110
- __versionSequences = new Map();
111
- /**
112
- * Texts pushed to the LSP textual language client via
113
- * {@link applyEditToLanguageClient} whose echoes have not come back yet
114
- * ({@link PendingPush}), keyed like the shadow by language-client URI.
115
- * Outbound pushes and inbound echoes are uncorrelated on the wire; this
116
- * FIFO is the explicit correlation, and it is what
117
- * {@link classifyLanguageClientChange} reconstructs against.
118
- *
119
- * **Each entry keeps the client's PRE-push text, not only a hash of the
120
- * post-push one.** A hash alone can classify a full-text echo, whose
121
- * application is a no-op either way — and that is all it ever classified,
122
- * because a conforming client echoes INCREMENTAL ranges keyed to its
123
- * previous buffer. Those ranges cannot be applied to the synced text (which
124
- * the authored write already advanced) and cannot be reconstructed without
125
- * the baseline: the line they insert lands twice, validates cleanly, and
126
- * compounds on every later edit.
127
- *
128
- * Lifecycle: entries are consumed by the matching echo (together with any
129
- * older entries it supersedes), and the whole queue drops when the client
130
- * stops being a pure mirror — a divergent change, an `applyEdit`
131
- * failure/rejection (shadow invalidation), a close, or an explicit
132
- * shadow (re)baseline. {@link PENDING_ECHO_CAP} bounds the queue against
133
- * a pathological echo that never arrives; the memory cost until then is
134
- * one pre-push text per in-flight push, for milliseconds.
135
- */
136
- __pendingPushes = new Map();
137
- /**
138
- * Tracked text content per URI for the LSP textual language client (Monaco / VS Code).
139
- * Owned here so {@link applyEditToLanguageClient} can compute minimal `workspace/applyEdit`
140
- * diffs instead of full-document replaces (5–20 s → <500 ms on 20-30 KB YAML diagrams).
141
- *
142
- * Auto-tracked from the multi-client text-document events: open / change / close of
143
- * the language client (re)baseline or invalidate the shadow. Other client ids
144
- * (form editor, GLSP, integrity) do NOT touch the shadow — only what Monaco believes
145
- * it has matters for the diff.
146
- *
147
- * Apply-verify safety net is built in: if the diff doesn't reconstruct `newText`
148
- * exactly, {@link LanguageClientTextShadow.computeEdits} falls back to a full-range
149
- * replace and invokes the `onFallback` callback — a diff regression becomes log
150
- * noise, not a 0-byte save.
151
- *
152
- * Assigned in the constructor body so the `onFallback` callback can capture
153
- * `this.logger` after the parameter-property assignment has run (field
154
- * initializers fire BEFORE parameter-property assignment in TS).
78
+ * Which client has which document open, which client ids are registered
79
+ * sessions, and each client's declared version, the staleness guard's
80
+ * baseline. Every open-state predicate on this class reads it, so an open
81
+ * recorded anywhere else is invisible to the last-close transition.
155
82
  */
156
- __shadow;
83
+ __sessions = new ClientSessionRegistry();
84
+ /** The version each open document was opened at; see {@link openedVersion}. */
85
+ __openedVersions = new Map();
157
86
  tracer;
158
87
  configuration;
88
+ __textLedger;
89
+ __languageClientShadow;
90
+ __dirtyStateTracker;
91
+ __documentReleaseScheduler;
92
+ documentReleasedEmitter = new Emitter();
93
+ /** Each release the `DocumentReleaseHandler` slot has not settled yet. */
94
+ releasesInFlight = new Set();
95
+ languageClientSavedEmitter = new Emitter();
159
96
  constructor(services, options = {}) {
160
97
  const configuration = options.configuration ?? TextDocument;
161
98
  super(configuration);
162
99
  this.services = services;
100
+ this.options = options;
163
101
  this.configuration = configuration;
164
102
  this.tracer = services.Tracer.for(options.logName ?? 'TextDocuments').trace('instantiated');
165
- this.__shadow = new LanguageClientTextShadow((uri, reason) => this.tracer.with(uri).warn(`Diff apply-verify fallback (${reason}) — using full-document replace`), this);
166
103
  }
167
- // Re-exposed configuration factories — for framework-internal callers
168
- // (LanguageClientTextShadow's apply-verify probe; IntegrityService.resyncDocument)
169
- // that need to materialise documents outside the canonical didOpen/didChange flow
170
- // and must respect the adopter's custom text-document type.
104
+ // Each collaborator is built on first use, after every constructor has run,
105
+ // so a create method may read its subclass's fields and the other collaborators.
106
+ get textLedger() {
107
+ return (this.__textLedger ??= this.createTextLedger());
108
+ }
109
+ get languageClientShadow() {
110
+ return (this.__languageClientShadow ??= this.createLanguageClientShadow());
111
+ }
112
+ get dirtyStateTracker() {
113
+ return (this.__dirtyStateTracker ??= this.createDirtyStateTracker());
114
+ }
115
+ get documentReleaseScheduler() {
116
+ return (this.__documentReleaseScheduler ??= this.createDocumentReleaseScheduler());
117
+ }
118
+ createTextLedger() {
119
+ return new DefaultTextLedger();
120
+ }
121
+ createLanguageClientShadow() {
122
+ return new DefaultLanguageClientShadow(this, this.tracer);
123
+ }
124
+ createDirtyStateTracker() {
125
+ return new DefaultDirtyStateTracker(this.textLedger);
126
+ }
127
+ createDocumentReleaseScheduler() {
128
+ return new DefaultDocumentReleaseScheduler(this.services.Clock, this.options.releaseGraceMs ?? DEFAULT_RELEASE_GRACE_MS);
129
+ }
130
+ /** Hold `document` as the text of `key`, a new version authored by `author`. */
131
+ commitText(key, document, author) {
132
+ this.__syncedDocuments.set(key, document);
133
+ this.setAuthor(key, document.version, author);
134
+ this.dirtyStateTracker.refreshDirty(key, document);
135
+ }
136
+ /**
137
+ * Apply `changes` to `document`, the held text of `key`, and hold the
138
+ * result. The version steps only when the text changes, which is what a
139
+ * base-version gate relies on: unchanged text keeps its version and its
140
+ * author. The new text is known only once the changes are applied, so they
141
+ * go in at a tentative step that an identical result rolls back.
142
+ */
143
+ commitChange(key, document, changes, author) {
144
+ const previousText = document.getText();
145
+ const version = document.version;
146
+ let next = this.update(document, changes, version + 1);
147
+ const changed = next.getText() !== previousText;
148
+ if (changed) {
149
+ this.commitText(key, next, author);
150
+ }
151
+ else {
152
+ // An empty-changes update only re-stamps the version.
153
+ next = this.update(next, [], version);
154
+ this.__syncedDocuments.set(key, next);
155
+ }
156
+ return { document: next, changed };
157
+ }
158
+ // The configuration's factories. Every document the store makes or changes
159
+ // goes through them, so an override sees each call: the store's own writes,
160
+ // callers outside the didOpen/didChange flow, and the shadow's throwaway
161
+ // probes, at version 0 under a client URI.
171
162
  create(uri, languageId, version, content) {
172
163
  return this.configuration.create(uri, languageId, version, content);
173
164
  }
@@ -221,7 +212,7 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
221
212
  connection.__textDocumentSync = TextDocumentSyncKind.Incremental;
222
213
  const disposables = new DisposableCollection();
223
214
  disposables.push(connection.onDidOpenTextDocument(async (event) => {
224
- await this.services.workspace.WorkspaceManager.workspaceInitialized;
215
+ await this.initialBuildFinished();
225
216
  this.notifyDidOpenTextDocument(event);
226
217
  }));
227
218
  disposables.push(connection.onDidChangeTextDocument((event) => {
@@ -235,7 +226,7 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
235
226
  }));
236
227
  disposables.push(connection.onWillSaveTextDocumentWaitUntil((event, token) => this.notifyWillSaveTextDocumentWaitUntil(event, token)));
237
228
  disposables.push(connection.onDidSaveTextDocument((event) => {
238
- this.notifyDidSaveTextDocument(event);
229
+ void this.notifyLanguageClientSave(event);
239
230
  }));
240
231
  return disposables;
241
232
  }
@@ -252,80 +243,58 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
252
243
  const uri = this.documentKey(td.uri);
253
244
  let document = this.__syncedDocuments.get(uri);
254
245
  if (document !== undefined) {
255
- // Per-client staleness guard: client version ids are CLIENT-owned per
256
- // LSP (Monaco numbers its own buffer), so an incoming id is compared
257
- // against THAT client's last declared id — never against the shared
258
- // version, which the server assigns and which routinely runs ahead of
259
- // a client's ids (authored ModelService writes advance it without the
260
- // client knowing). Gating on the shared version drops real edits in
261
- // exactly that lag window. A client with no baseline (never opened —
262
- // a protocol anomaly) falls back to the shared-version compare, the
263
- // conservative answer.
264
- const record = this.trackingFor(uri);
265
- const lastSeen = record.clientVersions.get(clientId) ?? document.version;
246
+ // Client version ids are the client's own, so compared against what
247
+ // that client declared, never against the shared version: an authored
248
+ // write advances that without the client knowing, and gating on it
249
+ // drops real edits. The editor is checked per URI, since each is its
250
+ // own buffer; its client-wide entry is only the fallback for a URI it
251
+ // never opened, and may name another URI's buffer. A client with no
252
+ // baseline falls back to the shared version.
253
+ const clientUri = this.toLanguageClientUri(td.uri);
254
+ const editor = clientId === LANGUAGE_CLIENT_ID;
255
+ const lastSeen = (editor ? this.languageClientShadow.declaredVersion(uri, clientUri) : undefined) ??
256
+ this.__sessions.clientVersionOf(uri, clientId) ??
257
+ document.version;
266
258
  if (lastSeen >= td.version) {
267
259
  // Distinguish "already at this version" (common: an echo from the client that triggered
268
260
  // the update) from "incoming version older than ours" (stale race).
269
261
  const reason = lastSeen === td.version ? `already at version ${lastSeen}` : `incoming version ${td.version} older than current ${lastSeen}`;
270
- this.logUri(uri, `Ignore update by ${clientId}: ${reason}`, 'debug');
262
+ this.logUri(uri, `Ignore update by ${this.formatClientId(clientId)}: ${reason}`, 'debug');
271
263
  return;
272
264
  }
273
- record.clientVersions.set(clientId, td.version);
274
- // A language-client change arriving while one of our own pushes is in
275
- // flight is keyed to the buffer the client held BEFORE that push, NOT
276
- // to the synced text — the authored write already advanced the latter.
277
- // Resolve which it is, and against what, before touching anything.
278
- const inFlight = clientId === LANGUAGE_CLIENT_ID
279
- ? this.classifyLanguageClientChange(this.toLanguageClientUri(td.uri), document, changes)
280
- : undefined;
281
- if (inFlight?.kind === 'echo') {
282
- // The client is reporting a text we pushed it, so the synced
283
- // document is ALREADY there and the changes must not be applied a
284
- // second time. Nothing is minted and no rebuild fires. The
285
- // per-client baseline advanced above (the client's ids keep
286
- // counting); the shadow is deliberately NOT touched — it
287
- // optimistically tracks the NEWEST pushed text, and dragging it
288
- // back would make the next outbound diff wrong against what the
289
- // client actually holds.
265
+ this.__sessions.setClientVersion(uri, clientId, td.version);
266
+ // An editor change is keyed to the buffer the editor holds, which is
267
+ // the synced text only while the two agree — an authored write
268
+ // advances the synced text without the editor knowing, and a push in
269
+ // flight moves the editor without the store knowing.
270
+ const verdict = editor
271
+ ? this.languageClientShadow.acceptChange(uri, clientUri, td.version, document, changes)
272
+ : { kind: 'direct' };
273
+ if (verdict.kind === 'echo') {
274
+ // The synced document is already there, so the changes must not be
275
+ // applied a second time: nothing is minted and no rebuild fires. The
276
+ // shadow keeps the newest pushed text, which is what the next
277
+ // outbound diff has to be keyed to.
290
278
  this.logUri(uri, `Skip rebuild: echo of a server-authored push (client version ${td.version})`, 'debug');
291
279
  return;
292
280
  }
293
- // The SHARED version advances iff the content actually changes — the
294
- // invariant optimistic `baseVersion` gates rely on. The new text is
295
- // only known after applying the (possibly incremental) changes, so
296
- // apply at a tentative +1 and roll the version back on an identical
297
- // result (an empty-changes update only re-stamps the version).
298
- //
299
- // A divergent in-flight change is applied as its RECONSTRUCTED text
300
- // rather than as its own ranges: the ranges address the pre-push
301
- // baseline, so applying them here would splice the wrong lines.
302
- const previousText = document.getText();
303
- const sharedVersion = document.version;
304
- document = this.configuration.update(document, inFlight === undefined ? changes : [{ text: inFlight.text }], sharedVersion + 1);
305
- const changed = document.getText() !== previousText;
306
- if (!changed) {
307
- document = this.configuration.update(document, [], sharedVersion);
308
- }
309
- this.__syncedDocuments.set(uri, document);
310
- if (changed) {
311
- this.setAuthor(uri, document.version, clientId);
312
- }
313
- if (clientId === LANGUAGE_CLIENT_ID) {
314
- // Monaco just told us about its new content; record it so the next outbound
315
- // applyEditToLanguageClient diffs against the right baseline. Keyed by the
316
- // language-client URI (what Monaco holds), not the canonical document key.
317
- this.__shadow.set(this.toLanguageClientUri(td.uri), document.getText());
318
- // Content-identical echo: the language client is echoing text we already had
319
- // (e.g. Monaco re-emitting a server-pushed applyEditToLanguageClient). The model is
320
- // unchanged, so skip the rebuild. The per-client baseline + shadow still advanced
321
- // above so future staleness checks and diffs are correct. Restricted to the
322
- // language client: a ModelService-authored change is never skipped.
323
- if (!changed) {
281
+ // A divergent change is applied as its reconstructed text rather than
282
+ // as its own ranges: those ranges address the editor's own buffer, so
283
+ // applying them here would splice the wrong lines.
284
+ const committed = this.commitChange(uri, document, verdict.kind === 'direct' ? changes : [{ text: verdict.text }], clientId);
285
+ document = committed.document;
286
+ if (editor) {
287
+ this.languageClientShadow.setLanguageClientText(clientUri, document.getText(), document.version);
288
+ // Content-identical echo: the editor is echoing text we already had.
289
+ // The model is unchanged, so skip the rebuild. Restricted to the
290
+ // editor: a ModelService-authored change is never skipped.
291
+ if (!committed.changed) {
324
292
  this.logUri(uri, `Skip rebuild: content unchanged (echo at client version ${td.version})`, 'debug');
325
293
  return;
326
294
  }
327
295
  }
328
- this.log(document.uri, `Update to version ${document.version} by ${clientId}${changed ? '' : ' (content unchanged)'}`);
296
+ // Diagnosis: one comes with every keystroke.
297
+ this.logUri(document.uri, `Update to version ${document.version} by ${this.formatClientId(clientId)}${committed.changed ? '' : ' (content unchanged)'}`, 'debug');
329
298
  this.__onDidChangeContent.fire(Object.freeze({ document, clientId }));
330
299
  }
331
300
  }
@@ -341,70 +310,188 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
341
310
  * A content-identical write still fires the change event (rebuild): the
342
311
  * authored write's server-side rebuild is correctness-bearing, it just
343
312
  * mints no new version — nothing observable changed, so watchers'
344
- * `baseVersion` pointers stay valid.
313
+ * base versions stay valid.
345
314
  *
346
315
  * Returns the resulting shared version. Throws when the document is not
347
316
  * open — callers (`AstDocumentManager.update`) open first.
348
317
  */
349
318
  applyContentChange(uri, text, clientId) {
350
319
  const key = this.documentKey(uri);
351
- let document = this.__syncedDocuments.get(key);
352
- if (document === undefined) {
320
+ const synced = this.__syncedDocuments.get(key);
321
+ if (synced === undefined) {
353
322
  throw new Error(`Document ${uri} is not open for content changes`);
354
323
  }
355
- const changed = document.getText() !== text;
356
- if (changed) {
357
- document = this.configuration.update(document, [{ text }], document.version + 1);
358
- this.__syncedDocuments.set(key, document);
359
- this.setAuthor(key, document.version, clientId);
360
- }
361
- this.log(document.uri, `Update to version ${document.version} by ${clientId}${changed ? '' : ' (content unchanged)'}`);
324
+ // Unchanged text calls no update: a configuration may return a new document for one.
325
+ const { document, changed } = synced.getText() === text ? { document: synced, changed: false } : this.commitChange(key, synced, [{ text }], clientId);
326
+ this.logUri(document.uri, `Update to version ${document.version} by ${this.formatClientId(clientId)}${changed ? '' : ' (content unchanged)'}`, 'debug');
362
327
  this.__onDidChangeContent.fire(Object.freeze({ document, clientId }));
363
328
  return document.version;
364
329
  }
365
- notifyDidCloseTextDocument(event, clientId = LANGUAGE_CLIENT_ID) {
330
+ /**
331
+ * Close `clientId`'s open of the document. When it was the last open, the
332
+ * document is released, at once or, for a `'lost'`
333
+ * close, after {@link HydraniumTextDocumentsOptions.releaseGraceMs}.
334
+ */
335
+ notifyDidCloseTextDocument(event, clientId = LANGUAGE_CLIENT_ID, cause = 'closed') {
366
336
  const uri = this.documentKey(event.textDocument.uri);
367
- if (!this.isOpenInClient(uri, clientId)) {
337
+ if (clientId === LANGUAGE_CLIENT_ID) {
338
+ const clientUri = this.toLanguageClientUri(event.textDocument.uri);
339
+ // A close under a URI the editor never opened the document under ends its hold.
340
+ if (this.languageClientShadow.isOpen(uri, clientUri)) {
341
+ this.languageClientShadow.removeOpen(uri, clientUri);
342
+ }
343
+ else {
344
+ this.languageClientShadow.removeAllOpens(uri);
345
+ }
346
+ // Another URI still holds the document, so only this one's buffer goes.
347
+ if (this.languageClientShadow.isOpen(uri)) {
348
+ return;
349
+ }
350
+ }
351
+ if (!this.__sessions.removeOpen(uri, clientId)) {
368
352
  return;
369
353
  }
370
- const closingRecord = this.__documents.get(uri);
371
- closingRecord?.clients.delete(clientId);
372
- closingRecord?.clientVersions.delete(clientId);
354
+ if (cause === 'lost') {
355
+ this.documentReleaseScheduler.recordLoss(uri, clientId);
356
+ }
373
357
  const syncedDocument = this.__syncedDocuments.get(uri);
374
358
  if (syncedDocument !== undefined) {
375
- this.log(syncedDocument.uri, `Closed synced document: ${syncedDocument.version} by ${clientId}`);
359
+ this.log(syncedDocument.uri, `Closed synced document: ${syncedDocument.version} by ${this.formatClientId(clientId)}`);
376
360
  this.__onDidClose.fire(Object.freeze({ document: syncedDocument, clientId }));
377
- if (clientId === LANGUAGE_CLIENT_ID) {
378
- // Monaco closed the document; drop the URI it held and the shadow
379
- // baselined under it. (If this was the last URI/client the whole record
380
- // is deleted below, clearing the set anyway.)
381
- const droppedUri = this.toLanguageClientUri(event.textDocument.uri);
382
- this.__documents.get(uri)?.languageClientUris?.delete(droppedUri);
383
- this.__shadow.invalidate(droppedUri);
384
- this.__pendingPushes.delete(droppedUri);
361
+ if (!this.isOpenInAnyClient(uri)) {
362
+ if (cause === 'lost') {
363
+ this.deferRelease(uri);
364
+ }
365
+ else {
366
+ this.releaseDocument(uri);
367
+ }
385
368
  }
386
- if (!this.__documents.get(uri)?.clients.size) {
387
- // Last client closed the document; delete sync state. The downstream
388
- // "rebuild from disk for file URIs / drop from index for ephemeral
389
- // URIs" decision lives in `HydraniumDocumentUpdateHandler.didCloseDocument`,
390
- // which subscribes to `onDidClose` and consults `isOpenInAnyClient`
391
- // to detect the last-close transition.
392
- this.log(syncedDocument.uri, `Remove synced document: ${syncedDocument.version} (no client left)`);
393
- // Persist where the shared version sequence left off (version +
394
- // content hash) so the next open CONTINUES the sequence instead of
395
- // restarting at the reopening client's declared id. Hashed once
396
- // here at close, not on every change.
397
- this.__versionSequences.set(uri, {
398
- version: syncedDocument.version,
399
- contentHash: contentHash(syncedDocument.getText())
400
- });
401
- this.__syncedDocuments.delete(uri);
402
- // One delete clears every per-URI axis (version history + any staged
403
- // pending content) so a future open with the same URI starts fresh.
404
- this.__documents.delete(uri);
369
+ }
370
+ }
371
+ /**
372
+ * Keep the document, text and all, for the release grace, then release it.
373
+ * The document stays in the store meanwhile, so a lost client that opens it
374
+ * again within its own grace attaches to it and finds its unsaved text
375
+ * rather than reading disk; see {@link resolveDeferredRelease} for any other
376
+ * open.
377
+ */
378
+ deferRelease(uri) {
379
+ this.documentReleaseScheduler.defer(uri, () => {
380
+ if (!this.isOpenInAnyClient(uri)) {
381
+ this.releaseDocument(uri);
382
+ }
383
+ });
384
+ if (this.documentReleaseScheduler.isDeferred(uri)) {
385
+ this.log(uri, `No client left; release deferred for ${this.documentReleaseScheduler.graceMs} ms (connection lost)`);
386
+ }
387
+ }
388
+ /** Resolve a deferred release of `uri` for an open by `clientId`: a release the scheduler decides on runs now. */
389
+ resolveDeferredRelease(uri, clientId) {
390
+ if (this.documentReleaseScheduler.resolveOpen(uri, clientId) === 'release') {
391
+ this.releaseDocument(uri);
392
+ }
393
+ }
394
+ /** Release now a document waiting out the grace, as its last close would have; one that is not waiting is left alone. */
395
+ releaseIfDeferred(uri) {
396
+ if (this.documentReleaseScheduler.isDeferred(uri)) {
397
+ this.documentReleaseScheduler.cancel(uri);
398
+ this.releaseDocument(uri);
399
+ }
400
+ }
401
+ /**
402
+ * Drop the document no client has open any more, announce it on
403
+ * {@link onDidReleaseDocument}, then hand it to the
404
+ * `DocumentReleaseHandler` slot: its listeners, such as the update
405
+ * handler dropping a change it still holds back, act before any build the
406
+ * handler runs.
407
+ */
408
+ releaseDocument(uri) {
409
+ const syncedDocument = this.__syncedDocuments.get(uri);
410
+ if (syncedDocument === undefined) {
411
+ return;
412
+ }
413
+ this.log(syncedDocument.uri, `Remove synced document: ${syncedDocument.version} (no client left)`);
414
+ // The next open continues the sequence instead of restarting at the
415
+ // reopening client's declared id.
416
+ this.textLedger.record(uri, syncedDocument);
417
+ this.textLedger.clearAuthors(uri);
418
+ // Before the tracker's release, which forgets whether a file was behind it.
419
+ const released = this.toReleasedDocument(uri);
420
+ const cleanAnnouncement = this.dirtyStateTracker.release(uri);
421
+ this.__syncedDocuments.delete(uri);
422
+ this.__openedVersions.delete(uri);
423
+ this.__sessions.forgetClientVersions(uri);
424
+ // A stage is for a first open; one released unconsumed is stale.
425
+ this.__pendingContent.delete(uri);
426
+ this.documentReleaseScheduler.clearLosses(uri);
427
+ this.documentReleasedEmitter.fire(Object.freeze({ uri }));
428
+ this.handOverRelease(released, cleanAnnouncement);
429
+ }
430
+ /**
431
+ * Call the `DocumentReleaseHandler` slot, and announce a document released
432
+ * dirty clean once the promise it returns settles, which is after the
433
+ * release event. A failure is logged rather than thrown: the store has let
434
+ * go of the document by now, and a throw would abort the transition that
435
+ * released it, a session's close of its other documents included.
436
+ */
437
+ handOverRelease(released, cleanAnnouncement) {
438
+ let settled;
439
+ try {
440
+ // Read here: a lazily built slot whose factory throws throws on this read.
441
+ const handler = this.services.workspace.DocumentReleaseHandler;
442
+ settled =
443
+ handler === undefined
444
+ ? Promise.reject(new Error('no workspace.DocumentReleaseHandler bound'))
445
+ : Promise.resolve(handler.didReleaseDocument(released));
446
+ }
447
+ catch (err) {
448
+ settled = Promise.reject(err);
449
+ }
450
+ // Names the text the build holds at the settle, so it follows anything a
451
+ // release listener did meanwhile: none when its build failed or it no
452
+ // longer has the document, since a removal leaves the record on the
453
+ // discarded text.
454
+ const announceClean = (built) => {
455
+ if (cleanAnnouncement?.isOwed()) {
456
+ const inBuild = built && this.services.workspace.LangiumDocuments.getDocument(UriUtils.toUri(released.uri)) !== undefined;
457
+ cleanAnnouncement.announce(inBuild ? this.textState(released.uri) : undefined);
405
458
  }
459
+ };
460
+ const handled = settled.then(() => announceClean(true), (err) => {
461
+ // A release skipped at teardown, its peer or its workspace gone, is
462
+ // routine, not a fault to investigate.
463
+ if (isDocumentReleaseSkippedError(err)) {
464
+ this.tracer.with(released.uri).debug(err.message);
465
+ }
466
+ else {
467
+ this.tracer.with(released.uri).error(`Release handler failed. ${describeErrorWithStack(err)}`);
468
+ }
469
+ // Left unsettled, a watcher keeps the document dirty though the store answers clean.
470
+ announceClean(false);
471
+ });
472
+ this.releasesInFlight.add(handled);
473
+ // A throw from the announcement surfaces once, as an unhandled rejection of the `finally` copy.
474
+ void handled.finally(() => this.releasesInFlight.delete(handled));
475
+ }
476
+ /**
477
+ * Resolves once the `DocumentReleaseHandler` slot has settled every
478
+ * release, including those handed over while waiting. A release still
479
+ * waiting out its grace has not been handed over.
480
+ */
481
+ async releasesSettled() {
482
+ while (this.releasesInFlight.size > 0) {
483
+ await Promise.allSettled(this.releasesInFlight);
406
484
  }
407
485
  }
486
+ /** `uri` as the `DocumentReleaseHandler` slot receives it. */
487
+ toReleasedDocument(uri) {
488
+ return {
489
+ uri,
490
+ wasOnDisk: this.dirtyStateTracker.hasDiskBaseline(uri),
491
+ isFor: other => this.documentKey(other) === uri,
492
+ isReclaimed: () => this.isOpenInAnyClient(uri) || this.__syncedDocuments.has(uri)
493
+ };
494
+ }
408
495
  notifyWillSaveTextDocument(event) {
409
496
  const syncedDocument = this.__syncedDocuments.get(this.documentKey(event.textDocument.uri));
410
497
  if (syncedDocument !== undefined) {
@@ -420,107 +507,172 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
420
507
  return [];
421
508
  }
422
509
  }
510
+ /**
511
+ * Handle a save the language client reports: fire
512
+ * {@link onDidSaveInLanguageClient}, then announce it on {@link onDidSave}
513
+ * only once the file read back from disk holds the store's current text.
514
+ */
515
+ async notifyLanguageClientSave(event) {
516
+ const syncedDocument = this.__syncedDocuments.get(this.documentKey(event.textDocument.uri));
517
+ if (syncedDocument === undefined) {
518
+ return;
519
+ }
520
+ const uri = syncedDocument.uri;
521
+ this.languageClientSavedEmitter.fire(Object.freeze({ uri }));
522
+ // `onDidSave` tells its listeners that the shared document is on disk.
523
+ // The editor wrote its own buffer, which lags the store while another
524
+ // client's edit is still on its way to it, so only the file says
525
+ // whether that holds. Read it rather than trust the editor: `didSave`
526
+ // carries text only for a client that honours `includeText`, and asking
527
+ // for it means rewriting Langium's initialize answer, which advertises
528
+ // `save` as a Boolean. The read goes through the disk queue and waits on
529
+ // nothing but the provider, so it follows a server save queued
530
+ // meanwhile. A file changed again between the editor's write and this
531
+ // read compares unequal, and the save goes unannounced.
532
+ let onDisk;
533
+ try {
534
+ onDisk = await this.services.workspace.FileSystemTaskQueue.enqueue(uri, () => this.services.workspace.FileSystemProvider.readFile(UriUtils.toUri(uri)));
535
+ }
536
+ catch (err) {
537
+ this.tracer.with(uri).debug(`Editor save not announced: reading the file back failed. ${describeError(err)}`);
538
+ return;
539
+ }
540
+ // What the file holds, not what the editor meant to write: an editor
541
+ // that saved older text leaves the document dirty.
542
+ this.setDiskBaseline(uri, onDisk);
543
+ // An editor that saves and then closes releases the document while the
544
+ // read is under way; its save is still a save of the text it held.
545
+ const document = this.__syncedDocuments.get(this.documentKey(uri)) ?? syncedDocument;
546
+ if (onDisk !== document.getText()) {
547
+ this.tracer.with(uri).debug(`Editor save not announced: the file does not hold the text of version ${document.version}`);
548
+ return;
549
+ }
550
+ this.announceSave(document, LANGUAGE_CLIENT_ID);
551
+ }
552
+ /**
553
+ * Announce a save the server made. `event.text`, when given, is what the
554
+ * file now holds, written or found there, and becomes the disk baseline
555
+ * before the save is announced. It lags the store when an edit landed
556
+ * after the save took its text, and the document then stays dirty.
557
+ */
423
558
  notifyDidSaveTextDocument(event, clientId = LANGUAGE_CLIENT_ID) {
424
559
  const syncedDocument = this.__syncedDocuments.get(this.documentKey(event.textDocument.uri));
425
560
  if (syncedDocument !== undefined) {
426
- this.log(syncedDocument.uri, `Saved synced document: ${syncedDocument.version} by ${clientId}`);
427
- this.__onDidSave.fire(Object.freeze({ document: syncedDocument, clientId }));
561
+ if (event.text !== undefined) {
562
+ this.setDiskBaseline(syncedDocument.uri, event.text);
563
+ }
564
+ this.announceSave(syncedDocument, clientId);
428
565
  }
429
566
  }
567
+ /** Fire {@link onDidSave} for `document`, which need not be synced any more. */
568
+ announceSave(document, clientId) {
569
+ this.log(document.uri, `Saved synced document: ${document.version} by ${this.formatClientId(clientId)}`);
570
+ this.__onDidSave.fire(Object.freeze({ document, clientId }));
571
+ }
430
572
  notifyDidOpenTextDocument(event, clientId = LANGUAGE_CLIENT_ID) {
431
573
  const td = event.textDocument;
432
574
  const uri = this.documentKey(td.uri);
433
- if (this.isOpenInClient(uri, clientId)) {
434
- // Already open for this client under this canonical identity. If this is a
435
- // NEW client-facing URI for the same file (a second tab reached via a
436
- // divergent path, e.g. a symlink and its real path), record it so outbound
437
- // edits reach this tab too — but do NOT re-fire open/rebuild; the document
438
- // is already live.
439
- if (clientId === LANGUAGE_CLIENT_ID) {
440
- const clientFacing = this.toLanguageClientUri(td.uri);
441
- const record = this.__documents.get(uri);
442
- if (record && !record.languageClientUris?.has(clientFacing)) {
443
- (record.languageClientUris ??= new Set()).add(clientFacing);
444
- // This tab's own buffer, NOT the synced text: a second tab is a second
445
- // client model, read from disk, so a server-authored write already
446
- // applied to the first tab leaves it BEHIND the synced document. Keying
447
- // a diff to the synced text here addresses lines this tab does not have
448
- // and drops content it does.
449
- this.__shadow.setOpenedText(clientFacing, td.text);
450
- }
451
- }
452
- return;
575
+ // Before the deferred release is resolved: refused after it, the open
576
+ // would leave a document whose grace it ended with no holder and no timer.
577
+ // A repeat open stays a no-op.
578
+ if (!this.__sessions.isOpenIn(uri, clientId)) {
579
+ this.__sessions.assertCanOpen(clientId);
453
580
  }
581
+ this.resolveDeferredRelease(uri, clientId);
454
582
  let document = this.__syncedDocuments.get(uri);
455
- const record = this.trackingFor(uri);
456
- const existingClients = [...record.clients];
457
- record.clients.add(clientId);
458
- // Baseline the per-client staleness guard at the version id the client
459
- // declared for its own buffer (client-owned per LSP).
460
- record.clientVersions.set(clientId, td.version);
461
- if (clientId === LANGUAGE_CLIENT_ID) {
462
- // Remember the URI Monaco opened under (may differ from the canonical key)
463
- // so outbound applyEditToLanguageClient can address the URI it actually holds.
464
- (record.languageClientUris ??= new Set()).add(this.toLanguageClientUri(td.uri));
583
+ const editor = clientId === LANGUAGE_CLIENT_ID;
584
+ // The client's declared text, never the synced document: staged
585
+ // content this open consumes leaves the two different, and a baseline
586
+ // asserting the client already holds it suppresses the one sync that
587
+ // would deliver it. On an attach, another client may already have
588
+ // changed the synced text, so the baseline is equality-only; a refresh
589
+ // pushing the full text would dirty the file on open. A first open's
590
+ // version is known only once its document is made, below.
591
+ if (editor) {
592
+ const held = document?.getText() === td.text ? document.version : undefined;
593
+ this.languageClientShadow.addOpen(uri, this.toLanguageClientUri(td.uri), td.version, td.text, document === undefined, held);
465
594
  }
595
+ if (this.isOpenInClient(uri, clientId)) {
596
+ // A repeat open, or the editor's second URI for the file, e.g. a
597
+ // symlink and its real path: the shadow records it so pushes reach it,
598
+ // and nothing re-fires.
599
+ return;
600
+ }
601
+ const existingClients = this.__sessions.clientsOf(uri);
602
+ this.__sessions.addOpen(uri, clientId);
603
+ this.__sessions.setClientVersion(uri, clientId, td.version);
466
604
  if (!document) {
467
605
  // Use integrity-staged content if available, otherwise the client-provided (disk) text.
468
606
  const pendingText = this.consumePendingContent(uri);
469
607
  const text = pendingText ?? td.text;
470
608
  const source = pendingText ? ', source=pending' : '';
471
- // The SHARED version is server-assigned: continue the persisted
472
- // sequence — same version when the content is unchanged since the
473
- // last close (so watchers' `baseVersion` pointers stay valid), one
474
- // step when it changed (so no stale pointer can coincidentally pass
475
- // the optimistic gate). Only a first-ever open adopts the client's
476
- // declared id as the sequence seed.
477
- const sequence = this.__versionSequences.get(uri);
478
- const version = sequence === undefined ? td.version : sequence.contentHash === contentHash(text) ? sequence.version : sequence.version + 1;
479
- this.log(uri, `Open document: Version ${version} by ${clientId} [first client${source}]`);
480
- document = this.configuration.create(uri, td.languageId, version, text);
481
- this.__syncedDocuments.set(uri, document);
482
- this.setAuthor(uri, version, clientId);
483
- if (clientId === LANGUAGE_CLIENT_ID) {
484
- // Baseline the shadow to what Monaco just opened so the next outbound
485
- // applyEditToLanguageClient diffs against the right starting point. Keyed by the
486
- // language-client URI (what Monaco holds), not the canonical document key.
487
- //
488
- // The CLIENT's declared text, never the synced document: staged content
489
- // consumed above leaves the two different, and a baseline asserting the
490
- // client already holds the staged text suppresses the one sync that would
491
- // deliver it. Skipped entirely when a push is already outstanding for this
492
- // URI — an `applyEdit` to a closed file has the client open from disk and
493
- // apply afterwards, so that shadow records what it is about to hold and
494
- // disk text would key the next diff to a buffer nobody has.
495
- const clientFacing = this.toLanguageClientUri(td.uri);
496
- if (this.__shadow.get(clientFacing) === undefined) {
497
- this.__shadow.set(clientFacing, td.text);
498
- }
609
+ const version = this.textLedger.openingVersion(uri, text) ?? this.builtRootOpeningVersion(uri, text) ?? td.version;
610
+ if (editor && text === td.text) {
611
+ // What `addOpen` already recorded, now with the version it is held at.
612
+ this.languageClientShadow.setLanguageClientText(this.toLanguageClientUri(td.uri), text, version);
499
613
  }
614
+ this.log(uri, `Open document: Version ${version} by ${this.formatClientId(clientId)} [first client${source}]`);
615
+ document = this.create(uri, td.languageId, version, text);
616
+ this.commitText(uri, document, clientId);
617
+ this.__openedVersions.set(uri, version);
618
+ // The opener's text, not the staged content: a session's open read
619
+ // it from the file, and an editor opened its buffer from there. An
620
+ // editor that opens a buffer it never saved is taken as clean.
621
+ this.dirtyStateTracker.track(uri, document, td.text);
500
622
  const toFire = Object.freeze({ document, clientId });
501
623
  this.__onDidOpen.fire(toFire);
502
624
  this.__onDidChangeContent.fire(toFire);
503
625
  }
504
626
  else {
505
627
  // An additional client attaches to a document already open by another client.
506
- this.log(uri, `Attach client: ${clientId} joined existing document (version ${document.version}, ` +
507
- `now open in: ${[...existingClients, clientId].join(', ')})`);
508
- if (clientId === LANGUAGE_CLIENT_ID) {
509
- // Monaco's own buffer, which on this path is NOT the synced text — another
510
- // client opened the document and may already have changed it. Recorded as
511
- // an equality-only baseline (never a diff one, see `setOpenedText`) so the
512
- // refresh below does not push a full replace of content Monaco already
513
- // holds, which would dirty the file on open.
514
- this.__shadow.setOpenedText(this.toLanguageClientUri(td.uri), td.text);
515
- }
628
+ this.logClientJoined(uri, clientId, document.version, existingClients);
516
629
  this.refreshContent(uri, clientId);
517
630
  }
518
631
  }
632
+ /**
633
+ * Record `clientId` as an additional holder of a document another client
634
+ * already has open.
635
+ *
636
+ * Unlike the attach branch of {@link notifyDidOpenTextDocument} this does
637
+ * not refresh. That branch re-renders an attaching TEXTUAL view; a client
638
+ * reaching a document through a non-textual route has nothing to re-render,
639
+ * and refreshing per attach turns each into a Langium rebuild and dependent
640
+ * relink cascade.
641
+ *
642
+ * Returns whether a hold was added — `false` when `uri` is not open,
643
+ * `clientId` already holds it, or the document was waiting out the release
644
+ * grace for other clients and was released instead (see
645
+ * {@link resolveDeferredRelease}); the caller then opens it anew.
646
+ */
647
+ attachClient(uri, clientId) {
648
+ const key = this.documentKey(uri);
649
+ if (!this.__syncedDocuments.has(key) || this.isOpenInClient(key, clientId)) {
650
+ return false;
651
+ }
652
+ // Before the deferred release is resolved, as for an open.
653
+ this.__sessions.assertCanOpen(clientId);
654
+ this.resolveDeferredRelease(key, clientId);
655
+ const document = this.__syncedDocuments.get(key);
656
+ if (!document) {
657
+ return false;
658
+ }
659
+ const existingClients = this.__sessions.clientsOf(key);
660
+ this.__sessions.addOpen(key, clientId);
661
+ // A client arriving this way holds no buffer of its own, so its guard
662
+ // starts at the synced version.
663
+ this.__sessions.setClientVersion(key, clientId, document.version);
664
+ this.logClientJoined(key, clientId, document.version, existingClients);
665
+ return true;
666
+ }
667
+ logClientJoined(uri, clientId, version, existingClients) {
668
+ this.log(uri, `Attach client: ${this.formatClientId(clientId)} joined existing document (version ${version}, ` +
669
+ `now open in: ${[...existingClients, clientId].map(id => this.formatClientId(id)).join(', ')})`);
670
+ }
519
671
  refreshContent(uri, clientId) {
520
672
  const syncedDocument = this.__syncedDocuments.get(this.documentKey(uri));
521
673
  if (syncedDocument) {
522
674
  // Trigger a (re-)build by firing a change event.
523
- const timer = this.startTimerForUri(syncedDocument.uri, `Refresh synced document: Version ${syncedDocument.version} by ${clientId}`);
675
+ const timer = this.startTimerForUri(syncedDocument.uri, `Refresh synced document: Version ${syncedDocument.version} by ${this.formatClientId(clientId)}`);
524
676
  this.__onDidChangeContent.fire(Object.freeze({ document: syncedDocument, clientId }));
525
677
  timer.dispose();
526
678
  }
@@ -532,7 +684,7 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
532
684
  * two URIs for one physical file (a symlink path and its real path) into a
533
685
  * single registration — dedup at the editor layer, not just in
534
686
  * `LangiumDocuments`. The URI the client opened under is preserved separately
535
- * for egress addressing (see {@link DocumentTracking.languageClientUris}). The
687
+ * for egress addressing, by the {@link LanguageClientShadow}. The
536
688
  * policy is always bound (the framework defaults it to
537
689
  * `DefaultDocumentUriPolicy`, where canonical ≡ syntactic normalize).
538
690
  */
@@ -550,17 +702,8 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
550
702
  toLanguageClientUri(uri) {
551
703
  return asLanguageClientUri(UriUtils.normalize(uri));
552
704
  }
553
- /** Get-or-create the per-URI tracking record. `uri` must already be a {@link documentKey}. */
554
- trackingFor(uri) {
555
- let record = this.__documents.get(uri);
556
- if (!record) {
557
- record = { clients: new Set(), versionAuthors: [], clientVersions: new Map() };
558
- this.__documents.set(uri, record);
559
- }
560
- return record;
561
- }
562
705
  setAuthor(uri, version, author) {
563
- this.trackingFor(this.documentKey(uri)).versionAuthors[version] = author;
706
+ this.textLedger.setAuthor(this.documentKey(uri), version, author);
564
707
  }
565
708
  /**
566
709
  * Resolve the synced `TextDocument` for `uri`. The store keys documents by
@@ -583,55 +726,190 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
583
726
  }
584
727
  /**
585
728
  * Current SHARED version of the document at `uri`: the open document's
586
- * version, else where the persisted sequence left off at last close, else
587
- * `0` for a URI this store has never seen. The shared sequence is
729
+ * version, else where its persisted sequence left off, else `0` for a URI
730
+ * this store has never seen. The shared sequence is
588
731
  * server-owned and monotonic across close/reopen cycles, and advances
589
732
  * exactly when the synced content changes — which is what makes it a sound
590
- * optimistic-concurrency token (`baseVersion` gates): version unchanged ⇔
733
+ * optimistic-concurrency token (base-version gates): version unchanged ⇔
591
734
  * content unchanged.
592
735
  */
593
736
  version(uri) {
594
- return this.get(uri)?.version ?? this.__versionSequences.get(this.documentKey(uri))?.version ?? 0;
737
+ return this.get(uri)?.version ?? this.textLedger.recordOf(this.documentKey(uri))?.version ?? 0;
738
+ }
739
+ /**
740
+ * The version the document at `uri` was opened at, which a client's write
741
+ * or an integrity repair steps past; `undefined` while it is not open.
742
+ */
743
+ openedVersion(uri) {
744
+ return this.__openedVersions.get(this.documentKey(uri));
745
+ }
746
+ /**
747
+ * Refuse unless the editor's buffer under `uri`, at its own `version`, holds
748
+ * the synced text, so a save the editor asks for persists what it shows.
749
+ * The editor's version counts its own buffer and the store's counts the
750
+ * synced text, so neither stands in for the other: `version` has to be the
751
+ * one the editor last declared, and the shadow has to know the buffer to
752
+ * hold the store's current version, which a push in flight breaks.
753
+ *
754
+ * A refusal is a {@link ConflictError} based on the store version the buffer
755
+ * last held, or on {@link STALE_VERSION} when it never held one or `version`
756
+ * names a buffer the editor never declared. It is a
757
+ * {@link DocumentNotOpenError} when the editor does not have `uri` open, or
758
+ * a delete of it is under way, which a save landing before the deletion
759
+ * build would undo by writing the file back.
760
+ */
761
+ assertLanguageClientHolds(uri, version) {
762
+ const key = this.documentKey(uri);
763
+ const clientUri = this.toLanguageClientUri(uri);
764
+ const declared = this.languageClientShadow.declaredVersion(key, clientUri);
765
+ const document = this.__syncedDocuments.get(key);
766
+ if (declared === undefined || document === undefined || this.services.workspace.AstDocumentManager.isDeleting(key)) {
767
+ throw new DocumentNotOpenError(uri, LANGUAGE_CLIENT_ID);
768
+ }
769
+ const held = declared === version ? this.languageClientShadow.heldVersion(key, clientUri) : undefined;
770
+ if (held !== document.version) {
771
+ const base = held ?? STALE_VERSION;
772
+ this.logUri(key, `Refuse editor save: buffer v${version} holds text v${base}, store holds v${document.version}`, 'debug');
773
+ // A snapshot, as the brand means: recorded when the buffer held that text, not read live.
774
+ throw new ConflictError(uri, asModelVersion(base), document.version, STALE_BASE_VERSION_SAVE);
775
+ }
776
+ }
777
+ /**
778
+ * Whether the editor holds text older than the store's text at `version`,
779
+ * or text the store never held. The first is the case while the push of
780
+ * that text is in flight. An answer computed on the store's text at
781
+ * `version` then refers to text the editor does not have. `false` once the
782
+ * editor holds that text or a later one, and when it does not have `uri`
783
+ * open.
784
+ */
785
+ isLanguageClientBehind(uri, version) {
786
+ const key = this.documentKey(uri);
787
+ const clientUri = this.toLanguageClientUri(uri);
788
+ if (!this.languageClientShadow.isOpen(key, clientUri)) {
789
+ return false;
790
+ }
791
+ const held = this.languageClientShadow.heldVersion(key, clientUri);
792
+ return held === undefined || held < version;
793
+ }
794
+ /**
795
+ * The text the store holds for `uri`: an open document's, or for a closed
796
+ * one, the clean text its version sequence left off at, which for a deleted
797
+ * document is its last text. `undefined` for a URI no build and no client
798
+ * gave the store.
799
+ */
800
+ textState(uri) {
801
+ const document = this.get(uri);
802
+ if (document) {
803
+ return { version: document.version, hash: this.textLedger.hashOf(document), dirty: this.isDirty(uri) };
804
+ }
805
+ const recorded = this.textLedger.recordOf(this.documentKey(uri));
806
+ return recorded && { version: recorded.version, hash: recorded.hash, dirty: false };
807
+ }
808
+ /**
809
+ * The built root's recorded version for a first open of `key` with `text`,
810
+ * one on when the text differs from the root's; `undefined` when the root
811
+ * records no store version. Seeded from the opener's declared version
812
+ * instead, a write based on the built root passes the gate over other text,
813
+ * and the same text looks newer than its model. A built root records none
814
+ * only under a `LangiumDocuments` that does not reconcile at registration.
815
+ */
816
+ builtRootOpeningVersion(key, text) {
817
+ const built = this.services.workspace.LangiumDocuments.getDocument(UriUtils.toUri(key));
818
+ if (built === undefined) {
819
+ return undefined;
820
+ }
821
+ const ledger = this.services.workspace.ModelLedger;
822
+ const root = built.parseResult.value;
823
+ const recorded = ledger.versionOf(root);
824
+ if (recorded === UNRECORDED_VERSION || recorded === STALE_VERSION) {
825
+ return undefined;
826
+ }
827
+ return (ledger.textOf(root) ?? built.textDocument.getText()) === text ? recorded : recorded + 1;
595
828
  }
596
829
  /**
597
830
  * Reconcile the persisted version sequence with content that reached the
598
831
  * build OUTSIDE the store's write paths — a closed document rebuilt from
599
- * disk (last-close revert) or replaced by a watched-file change. Steps the
832
+ * disk after its release, or replaced by a watched-file change. Steps the
600
833
  * sequence iff `text` differs from the sequence's last-known content and
601
834
  * returns the resulting sequence version so the caller can re-stamp the
602
- * rebuilt document (see the `AstDocumentManager` Parsed-phase listener) —
835
+ * rebuilt document (`VersionSyncService.modelProduced`) —
603
836
  * keeping the "version advances iff content changes" invariant for
604
837
  * documents no client currently holds.
605
838
  *
606
- * Returns `undefined` — and does nothing — when the document is open (the
607
- * store's own content is authoritative; external transitions never reach
608
- * the Langium factory for open documents) or was never tracked (no gate
609
- * holder can hold a version pointer for it, and the early exit keeps
610
- * workspace-wide builds from hashing every untouched document).
839
+ * For an open document, its version when it holds `text`, since a build
840
+ * that read the file while the document was closed can finish after an
841
+ * open; else `undefined`, and the caller has to build the document again.
842
+ *
843
+ * A document without a sequence starts one at `0` with `text`: snapshots
844
+ * hand out versions for documents no client opened, so an uncounted change
845
+ * would let a write based on the old text pass the gate.
611
846
  */
612
847
  reconcileExternalContent(uri, text) {
613
848
  const key = this.documentKey(uri);
614
- if (this.__syncedDocuments.has(key)) {
615
- return undefined;
849
+ const open = this.__syncedDocuments.get(key);
850
+ if (open !== undefined) {
851
+ return open.getText() === text ? open.version : undefined;
616
852
  }
617
- const sequence = this.__versionSequences.get(key);
618
- if (sequence === undefined) {
619
- return undefined;
853
+ const before = this.textLedger.recordOf(key)?.version;
854
+ const version = this.textLedger.reconcile(key, text);
855
+ if (before !== undefined && version !== before) {
856
+ this.logUri(key, `External content change while closed: sequence stepped to version ${version}`, 'debug');
857
+ }
858
+ return version;
859
+ }
860
+ /**
861
+ * Commit an integrity repair into the OPEN document for `uri`, returning the
862
+ * document that now holds it, or `undefined` when there was nothing here to
863
+ * commit into.
864
+ *
865
+ * The one thing this exists to do is address the store BY URI. A repair
866
+ * reaches the resync as a text-document object, and that object is the
867
+ * store's own only on the LSP path; a document built for an already-open URI
868
+ * through `LangiumDocumentFactory.fromString` carries its own, so writing the
869
+ * repair into it corrects a copy the store does not know about and the editor
870
+ * never sees.
871
+ *
872
+ * `parsedFrom` is the text the repaired AST was parsed from, and a mismatch
873
+ * against the current content REFUSES the commit: the store has moved on,
874
+ * which means an editor change landed after that parse, and committing would
875
+ * overwrite a newer edit with a repair computed against text the user has
876
+ * already replaced. The build that change provokes recomputes the repair.
877
+ *
878
+ * Deliberately NOT compared by version. A separately created document seeds
879
+ * its own numbering, so requiring the two to agree would refuse every commit
880
+ * on the path this exists for. Content is the thing both sides can be held to.
881
+ *
882
+ * A changed text is a new version authored by {@link INTEGRITY_CLIENT_ID}.
883
+ * Kept at the old version, the repair is invisible to every reader keyed on
884
+ * versions: a write based on the unrepaired version passes the base-version gate
885
+ * and replaces the repair, and an echo filter credits the repair to the
886
+ * client whose edit it corrected. No change event is fired: the repair
887
+ * rides the build already under way, and an event would re-enter it.
888
+ */
889
+ commitRepair(uri, parsedFrom, repaired) {
890
+ const key = this.documentKey(uri);
891
+ const document = this.__syncedDocuments.get(key);
892
+ if (document === undefined) {
893
+ return { status: 'not-open' };
894
+ }
895
+ if (document.getText() !== parsedFrom) {
896
+ this.logUri(key, 'Refuse repair commit: the open document moved on from the text the AST was parsed from', 'debug');
897
+ return { status: 'stale' };
620
898
  }
621
- const hash = contentHash(text);
622
- if (hash === sequence.contentHash) {
623
- return sequence.version;
899
+ if (repaired === parsedFrom) {
900
+ return { status: 'committed', document };
624
901
  }
625
- const stepped = { version: sequence.version + 1, contentHash: hash };
626
- this.__versionSequences.set(key, stepped);
627
- this.logUri(key, `External content change while closed: sequence stepped to version ${stepped.version}`, 'debug');
628
- return stepped.version;
902
+ // Reassigned rather than mutated in place: the default configuration
903
+ // updates and returns the SAME instance, but an adopter-supplied one may
904
+ // return a new object, and the store must end up holding whichever it is.
905
+ const updated = this.commitChange(key, document, [{ text: repaired }], INTEGRITY_CLIENT_ID).document;
906
+ this.log(updated.uri, `Update to version ${updated.version} by ${this.formatClientId(INTEGRITY_CLIENT_ID)} (repair)`);
907
+ return { status: 'committed', document: updated };
629
908
  }
630
909
  getAuthor(uri, version) {
631
- const history = this.__documents.get(this.documentKey(uri))?.versionAuthors;
632
- // Either the requested version, or the latest. `version !== undefined` so we treat 0 correctly.
633
- const clientId = version !== undefined ? history?.[version] : history?.at(-1);
634
- if (!clientId && history) {
910
+ const key = this.documentKey(uri);
911
+ const clientId = this.textLedger.authorOf(key, version);
912
+ if (!clientId && this.textLedger.authorOf(key) !== undefined) {
635
913
  // Only warn when there IS a history but the specific version is missing; no history at all
636
914
  // means the document was rebuilt internally (e.g. by a project manager), not an error.
637
915
  this.log(uri, `Could not detect author of version ${version}.`);
@@ -645,16 +923,18 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
645
923
  * True iff any client still holds `uri` open. Distinct from {@link isOpen},
646
924
  * which reads `__syncedDocuments` — that map is cleared only AFTER the
647
925
  * `onDidClose` event fires for the last client, so `isOpen` returns `true`
648
- * during the close event itself. `isOpenInAnyClient` reads the tracking
649
- * record's `clients` set, which is updated BEFORE the fire, so an `onDidClose`
650
- * subscriber that finds this `false` knows the last client just closed and a
651
- * disk re-read / rebuild can proceed.
926
+ * during the close event itself. `isOpenInAnyClient` reads the open table
927
+ * ({@link __sessions}), which is updated BEFORE the fire, so an `onDidClose`
928
+ * subscriber that finds this `false` knows the last client just closed.
929
+ * What the build keeps is the release's: a subscriber that re-read or
930
+ * rebuilt the document would race the `DocumentReleaseHandler`, and after a
931
+ * lost connection the release waits out its grace.
652
932
  */
653
933
  isOpenInAnyClient(uri) {
654
- return (this.__documents.get(this.documentKey(uri))?.clients.size ?? 0) > 0;
934
+ return this.__sessions.isOpen(this.documentKey(uri));
655
935
  }
656
936
  isOpenInClient(uri, client) {
657
- return !!this.__documents.get(this.documentKey(uri))?.clients.has(client);
937
+ return this.__sessions.isOpenIn(this.documentKey(uri), client);
658
938
  }
659
939
  isOpenInLanguageClient(uri) {
660
940
  // Takes any URI form (canonicalizes internally via `documentKey`, like
@@ -662,14 +942,18 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
662
942
  // client-facing URI both resolve to the same registration.
663
943
  return this.isOpenInClient(uri, LANGUAGE_CLIENT_ID);
664
944
  }
945
+ /** The clients that have `uri` open, in the order they opened it. */
946
+ clientsOf(uri) {
947
+ return this.__sessions.clientsOf(this.documentKey(uri));
948
+ }
665
949
  isOnlyOpenInClient(uri, client) {
666
- const key = this.documentKey(uri);
667
- return this.__documents.get(key)?.clients.size === 1 && this.isOpenInClient(key, client);
950
+ const clients = this.__sessions.clientsOf(this.documentKey(uri));
951
+ return clients.length === 1 && clients[0] === client;
668
952
  }
669
953
  /**
670
954
  * Every document currently held open by at least one client, with the client
671
- * ids holding it. Reads the same per-URI tracking records as
672
- * {@link isOpenInAnyClient}, so it reflects a last-client close immediately.
955
+ * ids holding it. Reads the same open table as {@link isOpenInAnyClient}, so
956
+ * it reflects a last-client close immediately.
673
957
  *
674
958
  * Diagnostics-oriented — the server-state snapshot lists these so an operator
675
959
  * can see WHY a document is pinned: a document that lingers here after its
@@ -677,30 +961,252 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
677
961
  * under a shedding policy, explains why its CST is never shed).
678
962
  */
679
963
  openDocuments() {
680
- const result = [];
681
- for (const [uri, tracking] of this.__documents) {
682
- if (tracking.clients.size > 0) {
683
- result.push({ uri, clients: [...tracking.clients] });
964
+ return this.__sessions.openDocuments();
965
+ }
966
+ /** Fires once a client session has ended, after every document it had open was closed. */
967
+ get onDidCloseSession() {
968
+ return this.__sessions.onDidCloseSession;
969
+ }
970
+ /**
971
+ * Fires once a document no client has open is released: at its last close,
972
+ * or, for a lost client's last close, when its release grace runs out,
973
+ * another client opens it, or it is deleted, before the
974
+ * `DocumentReleaseHandler` slot is handed the document.
975
+ */
976
+ get onDidReleaseDocument() {
977
+ return this.documentReleasedEmitter.event;
978
+ }
979
+ /**
980
+ * Fires for every save the language client reports of a document it has
981
+ * open: the editor has written the file. {@link onDidSave} follows only
982
+ * when the file holds the store's text.
983
+ */
984
+ get onDidSaveInLanguageClient() {
985
+ return this.languageClientSavedEmitter.event;
986
+ }
987
+ /**
988
+ * Whether `uri` is waiting out the release grace: its last open closed with a
989
+ * lost connection, and it still holds its unsaved text. Such a document is
990
+ * open for no client, yet not closed either, so a caller that would persist
991
+ * a closed document's text to disk treats it as open.
992
+ */
993
+ isReleaseDeferred(uri) {
994
+ return this.documentReleaseScheduler.isDeferred(this.documentKey(uri));
995
+ }
996
+ /**
997
+ * Whether the store holds `uri` with text that differs from its disk
998
+ * baseline: what the server last knew the file to hold. `false` for a URI
999
+ * the store does not hold; a document waiting out the release grace is still
1000
+ * held.
1001
+ *
1002
+ * The baseline is the text a first open brought, or what the server wrote,
1003
+ * or read back after an editor's save or a watched-file change, so it can
1004
+ * trail a change to the file that none of these has seen yet. A check that
1005
+ * must know the file reads it instead.
1006
+ */
1007
+ isDirty(uri) {
1008
+ return this.dirtyStateTracker.isDirty(this.documentKey(uri));
1009
+ }
1010
+ /**
1011
+ * Fires each time the answer of {@link isDirty} changes. For a document
1012
+ * released dirty it fires once the `DocumentReleaseHandler` slot reports the
1013
+ * release settled: at the version of the text the build then holds, or
1014
+ * without text when the document is gone or the handler failed, though
1015
+ * {@link isDirty} answers clean from the release on.
1016
+ */
1017
+ get onDidChangeDirty() {
1018
+ return this.dirtyStateTracker.onDidChangeDirty;
1019
+ }
1020
+ /**
1021
+ * Record that the file behind `uri` holds `text`, or no file at all for
1022
+ * `undefined`. A no-op for a URI the store does not hold: the next first
1023
+ * open sets the baseline from its own text.
1024
+ */
1025
+ setDiskBaseline(uri, text) {
1026
+ const key = this.documentKey(uri);
1027
+ const document = this.__syncedDocuments.get(key);
1028
+ if (document !== undefined) {
1029
+ this.dirtyStateTracker.setDiskBaseline(key, document, text);
1030
+ }
1031
+ }
1032
+ /**
1033
+ * Record that `uri` was created from a session's text rather than read from
1034
+ * its file: it has no disk baseline, so it is dirty until its first save,
1035
+ * and {@link isUnsavedCreation} answers `true` until then. A no-op for a
1036
+ * URI the store does not hold.
1037
+ */
1038
+ markCreated(uri) {
1039
+ const key = this.documentKey(uri);
1040
+ const document = this.__syncedDocuments.get(key);
1041
+ if (document !== undefined) {
1042
+ this.dirtyStateTracker.markCreated(key, document);
1043
+ }
1044
+ }
1045
+ /**
1046
+ * Whether `uri` was created from a session's text and the store has not
1047
+ * seen its file since, by a save or a reload that found it. A baseline
1048
+ * cleared because the file went missing does not count. `false` for a URI
1049
+ * the store does not hold.
1050
+ */
1051
+ isUnsavedCreation(uri) {
1052
+ return this.dirtyStateTracker.isUnsavedCreation(this.documentKey(uri));
1053
+ }
1054
+ /**
1055
+ * Read the file behind `uri` through its disk queue and take it as the
1056
+ * baseline. A file that cannot be read counts as none, the side that
1057
+ * leaves the document dirty, once `existsSettled` confirms it gone: a file
1058
+ * it finds is read again. That wait runs outside the queue, so it holds up
1059
+ * no write to the file.
1060
+ */
1061
+ async reloadDiskBaseline(uri) {
1062
+ const key = this.documentKey(uri);
1063
+ if (!this.__syncedDocuments.has(key)) {
1064
+ return;
1065
+ }
1066
+ const workspace = this.services.workspace;
1067
+ const fileSystem = workspace.FileSystemProvider;
1068
+ const target = UriUtils.toUri(key);
1069
+ const read = async () => {
1070
+ try {
1071
+ return await workspace.FileSystemTaskQueue.enqueue(key, () => fileSystem.readFile(target));
1072
+ }
1073
+ catch (err) {
1074
+ this.tracer.with(key).debug(`Disk baseline: the file cannot be read. ${describeError(err)}`);
1075
+ return undefined;
1076
+ }
1077
+ };
1078
+ let onDisk = await read();
1079
+ if (onDisk === undefined && (await (fileSystem.existsSettled?.(target) ?? fileSystem.exists(target)))) {
1080
+ onDisk = await read();
1081
+ }
1082
+ this.setDiskBaseline(key, onDisk);
1083
+ }
1084
+ /**
1085
+ * Start a client session under `clientId`. Throws where
1086
+ * {@link ClientSessionRegistry.register} refuses the id.
1087
+ */
1088
+ registerSession(clientId) {
1089
+ this.__sessions.register(clientId);
1090
+ this.tracer.info(`Session started: ${this.formatClientId(clientId)}`);
1091
+ this.tracer.trace(`Session started: ${clientId}`);
1092
+ }
1093
+ /**
1094
+ * End the client session `clientId`: close every document it has open, then
1095
+ * free the id. Immediate — each close runs the ordinary close path before
1096
+ * this returns, with `cause` as its cause. A no-op for an id that is not a
1097
+ * registered session.
1098
+ */
1099
+ closeSession(clientId, cause = 'closed') {
1100
+ if (!this.__sessions.isRegistered(clientId)) {
1101
+ return;
1102
+ }
1103
+ try {
1104
+ for (const uri of this.__sessions.beginClose(clientId)) {
1105
+ this.notifyDidCloseTextDocument({ textDocument: { uri } }, clientId, cause);
1106
+ }
1107
+ }
1108
+ finally {
1109
+ this.__sessions.unregister(clientId, cause);
1110
+ this.tracer.info(`Session closed: ${this.formatClientId(clientId)}`);
1111
+ this.tracer.trace(`Session closed: ${clientId}`);
1112
+ }
1113
+ }
1114
+ /**
1115
+ * Close every document the language client has open, as a `didClose` for
1116
+ * each would, so each last close releases its document. For a host whose editor connection
1117
+ * can end while the process lives on, such as a worker whose port's peer
1118
+ * closed: the language client is no session, so nothing else closes them.
1119
+ *
1120
+ * It first waits for the workspace initialization the open handler waits
1121
+ * for, so an open that arrived before the close is closed too; closing at
1122
+ * once would leave that open to land afterwards, held by a client that is
1123
+ * gone.
1124
+ */
1125
+ async closeLanguageClientDocuments() {
1126
+ await this.initialBuildFinished();
1127
+ for (const uri of this.__sessions.opensOf(LANGUAGE_CLIENT_ID)) {
1128
+ // A close for one URI while others remain keeps the client's hold.
1129
+ this.languageClientShadow.removeAllOpens(uri);
1130
+ this.notifyDidCloseTextDocument({ textDocument: { uri } });
1131
+ }
1132
+ }
1133
+ /**
1134
+ * Settles once the initial workspace build has finished, whether it
1135
+ * completed, was cancelled or failed. Awaiting `workspaceInitialized` itself
1136
+ * throws after a cancelled build, which any write during startup causes, and
1137
+ * every open and close gated on it is then dropped for the process lifetime.
1138
+ * The workspace manager logs a failed build.
1139
+ */
1140
+ async initialBuildFinished() {
1141
+ await this.services.workspace.WorkspaceManager.workspaceInitialized.catch(() => undefined);
1142
+ }
1143
+ /**
1144
+ * The file behind `uri` was deleted: close every open of it except the
1145
+ * language client's, and release now a document waiting out the grace,
1146
+ * whose lost client's open would otherwise bring its text back with no
1147
+ * file behind it.
1148
+ *
1149
+ * The editor's open is left alone because the editor owns it: it keeps the
1150
+ * buffer of a deleted file and goes on sending changes for it, and a store
1151
+ * that had closed the document would drop every one of them until the editor
1152
+ * reopened.
1153
+ */
1154
+ notifyDocumentDeleted(uri) {
1155
+ const key = this.documentKey(uri);
1156
+ for (const clientId of this.__sessions.clientsOf(key)) {
1157
+ if (clientId !== LANGUAGE_CLIENT_ID) {
1158
+ this.notifyDidCloseTextDocument({ textDocument: { uri: key } }, clientId);
684
1159
  }
685
1160
  }
686
- return result;
1161
+ this.releaseIfDeferred(key);
687
1162
  }
688
1163
  /**
689
- * Stages integrity-updated content for a closed document.
1164
+ * Remove the document for `uri`, closing every open of it first — the
1165
+ * editor's included, since the document it would keep editing is gone.
690
1166
  *
691
- * When `workspace/applyEdit` targets a closed file, the client opens it from
692
- * disk (stale) and sends `didOpen` before applying the edit. This staged
693
- * content is consumed by {@link notifyDidOpenTextDocument} to replace the
694
- * stale disk text, preventing a brief revert of the integrity update.
1167
+ * The base removes the synced document without consulting the open table,
1168
+ * which leaves every open recorded against a document that no longer exists:
1169
+ * the store then answers that the URI is open, and the next open attaches to
1170
+ * nothing.
1171
+ */
1172
+ delete(uri) {
1173
+ const key = this.documentKey((typeof uri === 'object' && 'uri' in uri ? uri.uri : uri).toString());
1174
+ this.languageClientShadow.removeAllOpens(key);
1175
+ for (const clientId of this.__sessions.clientsOf(key)) {
1176
+ this.notifyDidCloseTextDocument({ textDocument: { uri: key } }, clientId);
1177
+ }
1178
+ // A document waiting out the grace has no client left to close, and is
1179
+ // released now as its last close would have released it.
1180
+ this.releaseIfDeferred(key);
1181
+ super.delete(key);
1182
+ }
1183
+ /**
1184
+ * A client id as log lines print it. An id containing `#` is cut eight
1185
+ * characters after its last `#`, which leaves a minted `label#uuid` as the
1186
+ * label and enough of the UUID to tell sessions apart; an id without `#`
1187
+ * prints whole. The full id goes out at trace level wherever a session
1188
+ * starts or ends.
1189
+ */
1190
+ formatClientId(clientId) {
1191
+ const separator = clientId.lastIndexOf('#');
1192
+ return separator < 0 ? clientId : clientId.slice(0, separator + 9);
1193
+ }
1194
+ /**
1195
+ * Stages integrity-updated content for a document no client holds.
1196
+ *
1197
+ * Nothing is pushed to a file no client holds. The next open of it reads
1198
+ * disk, which lacks the update; {@link notifyDidOpenTextDocument} takes this
1199
+ * staged content in place of that text, and the open's sync then delivers
1200
+ * it to an editor as an unsaved change.
695
1201
  *
696
- * Not cleared on close: pending content is only staged for already-closed
697
- * documents (the integrity service checks `isOpenInLanguageClient` first), so
698
- * a close → stage → open cycle cannot occur. The only scenario where an
699
- * entry lingers is if `workspace/applyEdit` fails and the file is never
700
- * opened — the memory cost is one serialised string per URI.
1202
+ * Only a FIRST open consumes it, so stage only for a URI no client holds
1203
+ * ({@link isOpenInAnyClient} is `false`). A URI held only through another
1204
+ * head is not closed: an editor attaching to it joins the existing entry and
1205
+ * never reads the stage, and the release discards it. An entry waits for
1206
+ * that first open, at one serialised string per URI.
701
1207
  */
702
1208
  stagePendingContent(uri, text) {
703
- this.trackingFor(this.documentKey(uri)).pendingContent = text;
1209
+ this.__pendingContent.set(this.documentKey(uri), text);
704
1210
  }
705
1211
  /**
706
1212
  * Send `newText` to the LSP textual language client (Monaco / VS Code) via
@@ -711,63 +1217,53 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
711
1217
  * - The shared services have no LSP {@link Connection} bound (non-LSP
712
1218
  * hosts like CLI / tests).
713
1219
  * - The shadow already matches `newText` (no edits needed; quiet skip).
1220
+ * - The language client has not opened the document. A client applies an
1221
+ * edit to a closed file by opening it, and that open races the edit;
1222
+ * the document's own open delivers its text instead.
714
1223
  *
715
1224
  * On `applyEdit` rejection (`result.applied === false`) or RPC failure the
716
- * shadow is invalidated so the next call sends a full-replace baseline.
1225
+ * shadow is invalidated so the next call sends a full replace, or nothing
1226
+ * when the client was last heard to hold that text.
717
1227
  * Errors are re-thrown — callers wrap with their own retry / coalescing
718
1228
  * policy as needed.
719
1229
  *
720
1230
  * The diff path is apply-verify-safe: the shadow internally checks that
721
1231
  * `TextDocument.applyEdits(old, edits) === newText` and falls back to a
722
- * full replace on mismatch (logged via the warn-callback wired in the
723
- * constructor), so a diff regression becomes log noise, not data loss.
1232
+ * full replace on mismatch, logged, so a diff regression becomes log noise,
1233
+ * not data loss.
724
1234
  *
725
1235
  * That safety net verifies the diff against the SHADOW, which is what the
726
1236
  * client is *believed* to hold — so it cannot see the client's buffer moving
727
1237
  * underneath a push. A line-keyed edit is position-dependent: if a genuine
728
- * client keystroke lands between {@link LanguageClientTextShadow.computeEdits}
729
- * and the client applying, the ranges address the wrong lines and splice the
730
- * buffer (observed as a duplicated declaration, which the integrity tier then
1238
+ * client keystroke lands between computing the edits and the client
1239
+ * applying them, the ranges address the wrong lines and splice the buffer
1240
+ * (observed as a duplicated declaration, which the integrity tier then
731
1241
  * "repairs" into a suffixed name and persists). The edit is therefore
732
- * addressed at the language client's LAST DECLARED VERSION rather than at
733
- * `null` ("version intentionally unknown"), which is what lets the client
734
- * reject a push its buffer has outrun. On rejection the shadow is invalidated,
1242
+ * addressed at the language client's last known version for that URI rather
1243
+ * than at `null` ("version intentionally unknown"), which is what lets the
1244
+ * client reject a push its buffer has outrun. On rejection the shadow is invalidated,
735
1245
  * so the caller's retry is a full-range replace — position-independent, and
736
- * safe to apply to whatever the client now holds.
1246
+ * safe to apply to whatever the client now holds — or nothing when the
1247
+ * client was last heard to hold that text.
737
1248
  */
738
1249
  async applyEditToLanguageClient(uri, newText, options) {
739
1250
  const connection = this.services.lsp?.Connection;
740
1251
  if (!connection) {
741
1252
  return undefined;
742
1253
  }
743
- // The document is keyed by its canonical identity, but Monaco holds it under
744
- // the URI(s) it opened. Address each of those language-client URIs — the one
745
- // R→S translation, kept here at the egress. Usually one; more than one only
746
- // when the same file was opened under a symlink and its real path. Falls back
747
- // to the normalized URI when nothing is tracked. The shadow is keyed by each
748
- // URI, so every diff is against the right baseline.
749
- const recorded = this.__documents.get(this.documentKey(uri))?.languageClientUris;
750
- const targets = recorded && recorded.size > 0 ? recorded : [this.toLanguageClientUri(uri)];
1254
+ // The document is keyed by its canonical identity, but the client holds it
1255
+ // under each URI it opened, and each is diffed against its own baseline.
1256
+ const key = this.documentKey(uri);
751
1257
  let lastResult;
752
- for (const targetUri of targets) {
753
- // Read BEFORE computeEdits, which overwrites it. This is the text the
754
- // edits below are keyed to, and therefore the only text the client's
755
- // echo of them can be reconstructed against.
756
- const before = this.__shadow.get(targetUri);
757
- const edits = this.__shadow.computeEdits(targetUri, newText);
758
- if (edits.length === 0) {
1258
+ for (const clientUri of this.languageClientShadow.pushTargets(key)) {
1259
+ // Prepared per target, after the previous target's reply: prepared up
1260
+ // front, a later target is diffed against what it held before a change
1261
+ // that arrived meanwhile.
1262
+ const push = this.languageClientShadow.preparePush(key, clientUri, newText);
1263
+ if (push === undefined) {
759
1264
  continue;
760
1265
  }
761
- // Record the push for echo correlation BEFORE the RPC — the client's
762
- // echo can race the applyEdit response. See `__pendingPushes`.
763
- this.recordPendingPush(targetUri, before, newText);
764
1266
  try {
765
- // Version the push only when it is position-DEPENDENT. A full-range
766
- // replace lands correctly on any buffer, so gating it would turn a
767
- // stale-by-one version into a refused update for no safety gain — and
768
- // it is exactly what the caller retries with after a rejection, so
769
- // gating it there would refuse the recovery too.
770
- const version = isFullReplace(edits) ? UNKNOWN_CLIENT_VERSION : this.languageClientVersion(uri);
771
1267
  // A full `ApplyWorkspaceEditParams`, `edit` and all — NOT a bare
772
1268
  // `WorkspaceEdit` with a `label` beside it. `applyEdit` takes
773
1269
  // `ApplyWorkspaceEditParams | WorkspaceEdit` and discriminates on
@@ -777,137 +1273,55 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
777
1273
  // The union is also what hides it at compile time: excess-property
778
1274
  // checking admits a property present in EITHER member, so an object
779
1275
  // matching neither type-checks against the union.
1276
+ const version = push.version ?? UNKNOWN_CLIENT_VERSION;
780
1277
  const result = await connection.workspace.applyEdit({
781
1278
  label: options?.label,
782
1279
  edit: {
783
- documentChanges: [TextDocumentEdit.create(OptionalVersionedTextDocumentIdentifier.create(targetUri, version), edits)]
1280
+ documentChanges: [TextDocumentEdit.create(OptionalVersionedTextDocumentIdentifier.create(clientUri, version), push.edits)]
784
1281
  }
785
1282
  });
786
1283
  if (result && result.applied === false) {
787
- this.__shadow.invalidate(targetUri);
788
- this.__pendingPushes.delete(targetUri);
1284
+ push.notifyOutcome('refused');
1285
+ }
1286
+ else if (result?.applied) {
1287
+ push.notifyOutcome('applied');
789
1288
  }
790
1289
  lastResult = result;
791
1290
  }
792
1291
  catch (err) {
793
- this.__shadow.invalidate(targetUri);
794
- this.__pendingPushes.delete(targetUri);
1292
+ push.notifyOutcome('failed');
795
1293
  throw err;
796
1294
  }
797
1295
  }
798
1296
  return lastResult;
799
1297
  }
800
1298
  /**
801
- * The version the LSP textual language client last declared for `uri`, for
802
- * addressing an outgoing `workspace/applyEdit`.
803
- *
804
- * Client version ids are CLIENT-owned per LSP, so this is the id the client
805
- * itself stamped on its last `didOpen` / `didChange` — never the shared
806
- * server version, which advances on authored writes the client knows nothing
807
- * about and would therefore reject every push.
808
- *
809
- * Falls back to {@link UNKNOWN_CLIENT_VERSION} when the client has never
810
- * declared one, which is the honest answer for a document it has not opened.
811
- * That is also the case in which there is no shadow, so the push is already a
812
- * position-independent full replace and has nothing to gain from a gate.
813
- */
814
- languageClientVersion(uri) {
815
- return this.__documents.get(this.documentKey(uri))?.clientVersions.get(LANGUAGE_CLIENT_ID) ?? UNKNOWN_CLIENT_VERSION;
816
- }
817
- /**
818
- * Append a push to the in-flight queue for `targetUri`
819
- * (see {@link __pendingPushes}). Bounded: beyond
820
- * {@link PENDING_ECHO_CAP} the oldest entry drops with a debug log — an
821
- * echo that far outstanding means the client is not echoing at all, and
822
- * an unbounded queue must not become the leak.
1299
+ * Explicitly baseline the language-client shadow for a URI the client has
1300
+ * open, for an adopter that knows what the client holds without a
1301
+ * `didChange` saying so (e.g. after a sideband save). Normal didOpen /
1302
+ * didChange paths from the LSP language client already auto-track the
1303
+ * shadow. A seed for a URI the client has not opened is kept but changes no
1304
+ * push, since nothing is pushed there; its open replaces it with the text it
1305
+ * declares. `text` other than the synced text refuses the editor's saves
1306
+ * until the editor holds the synced text again.
823
1307
  */
824
- recordPendingPush(targetUri, before, newText) {
825
- let pending = this.__pendingPushes.get(targetUri);
826
- if (!pending) {
827
- pending = [];
828
- this.__pendingPushes.set(targetUri, pending);
829
- }
830
- pending.push({ before, afterHash: contentHash(newText) });
831
- if (pending.length > PENDING_ECHO_CAP) {
832
- pending.shift();
833
- this.logUri(targetUri, `Pending-echo queue exceeded ${PENDING_ECHO_CAP} entries; dropped the oldest`, 'debug');
834
- }
835
- }
836
- /**
837
- * Decide what an incoming language-client change actually is, by
838
- * reconstructing the client's resulting buffer against the OLDEST push still
839
- * in flight for `clientFacing`.
840
- *
841
- * `undefined` when nothing of ours is in flight, which is the ordinary path:
842
- * the client's ranges then address the synced text and the caller applies
843
- * them to it directly.
844
- *
845
- * **Why the oldest, and why reconstruct at all.** The client applies our
846
- * pushes in order and echoes each against the buffer it held before that
847
- * push, so the first echo to arrive belongs to the oldest entry — a FIFO
848
- * correspondence the queue preserves by consuming from the front. Comparing
849
- * the reconstruction against every pending hash, not only the oldest, is
850
- * what recognises an echo that a newer write already superseded: a rapid
851
- * write sequence can deliver the echo of push N after the store applied
852
- * push N+1, and everything older is then accounted for too.
853
- *
854
- * A reconstruction matching NO pending push means the client's buffer went
855
- * somewhere we did not send it — it coalesced a keystroke into the echo, or
856
- * typed before the push landed. That text is authoritative, and the queue
857
- * drops: the client has stopped being a pure mirror, so no outstanding echo
858
- * can match again. (A push still in flight at that point will be refused by
859
- * the client's own version gate, which invalidates the shadow and makes the
860
- * next sync a position-independent full replace.)
861
- *
862
- * Content equality is a sound echo proof because entries live only between a
863
- * push and its echo — a milliseconds window, never history — and the
864
- * client's `didChange` stream is ordered, so every buffer state arrives in
865
- * mutation order. An UNDO returning the buffer to previously-pushed text is
866
- * therefore never swallowed: the edit that preceded it already emptied the
867
- * queue, so undo revisits PAST states while the queue holds IN-FLIGHT ones.
868
- */
869
- classifyLanguageClientChange(clientFacing, document, changes) {
870
- const pending = this.__pendingPushes.get(clientFacing);
871
- if (pending === undefined || pending.length === 0) {
872
- return undefined;
873
- }
874
- // Through the configured factories, not `TextDocument` directly, so an
875
- // adopter's custom text-document type governs how the ranges are applied
876
- // here exactly as it does on the synced document.
877
- const probe = this.create(clientFacing, document.languageId, 0, pending[0].before ?? document.getText());
878
- const clientText = this.update(probe, changes, 0).getText();
879
- const matchIndex = pending.findIndex(push => push.afterHash === contentHash(clientText));
880
- if (matchIndex >= 0) {
881
- pending.splice(0, matchIndex + 1);
882
- return { kind: 'echo' };
883
- }
884
- pending.length = 0;
885
- return { kind: 'divergent', text: clientText };
1308
+ setLanguageClientText(uri, text) {
1309
+ const document = this.get(uri);
1310
+ const held = document?.getText() === text ? document.version : undefined;
1311
+ this.languageClientShadow.setLanguageClientText(this.toLanguageClientUri(uri), text, held);
886
1312
  }
887
1313
  /**
888
- * Explicitly baseline the language-client text shadow for a URI. Useful in
889
- * tests and for adopters that need to seed the shadow without going through
890
- * a `didOpen` event (e.g. after a sideband save). Normal didOpen / didChange
891
- * paths from the LSP language client already auto-track the shadow.
1314
+ * Drop the shadow baseline for a URI; the next applyEditToLanguageClient
1315
+ * sends a full replace, or nothing when the client was last heard to hold
1316
+ * that text.
892
1317
  */
893
- setLanguageClientText(uri, text) {
894
- const clientFacing = this.toLanguageClientUri(uri);
895
- this.__shadow.set(clientFacing, text);
896
- // An explicit rebaseline supersedes whatever was in flight.
897
- this.__pendingPushes.delete(clientFacing);
898
- }
899
- /** Drop the shadow baseline for a URI; the next applyEditToLanguageClient sends a full replace. */
900
1318
  invalidateLanguageClientText(uri) {
901
- const clientFacing = this.toLanguageClientUri(uri);
902
- this.__shadow.invalidate(clientFacing);
903
- this.__pendingPushes.delete(clientFacing);
1319
+ this.languageClientShadow.invalidateLanguageClientText(this.toLanguageClientUri(uri));
904
1320
  }
905
1321
  consumePendingContent(uri) {
906
- const record = this.__documents.get(this.documentKey(uri));
907
- const content = record?.pendingContent;
908
- if (record) {
909
- record.pendingContent = undefined;
910
- }
1322
+ const key = this.documentKey(uri);
1323
+ const content = this.__pendingContent.get(key);
1324
+ this.__pendingContent.delete(key);
911
1325
  return content;
912
1326
  }
913
1327
  log(uri, message) {