@hydranium/core 1.0.0-next.12 → 1.0.0-next.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/lib/documents/ast-document-manager.d.ts +92 -19
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +107 -26
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/client-ids.d.ts +5 -19
- package/lib/documents/client-ids.d.ts.map +1 -1
- package/lib/documents/client-ids.js +5 -19
- package/lib/documents/client-ids.js.map +1 -1
- package/lib/documents/client-session-errors.d.ts +15 -0
- package/lib/documents/client-session-errors.d.ts.map +1 -0
- package/lib/documents/client-session-errors.js +15 -0
- package/lib/documents/client-session-errors.js.map +1 -0
- package/lib/documents/client-session-registry.d.ts +138 -0
- package/lib/documents/client-session-registry.d.ts.map +1 -0
- package/lib/documents/client-session-registry.js +184 -0
- package/lib/documents/client-session-registry.js.map +1 -0
- package/lib/documents/file-system-task-queue.d.ts +49 -0
- package/lib/documents/file-system-task-queue.d.ts.map +1 -0
- package/lib/documents/file-system-task-queue.js +34 -0
- package/lib/documents/file-system-task-queue.js.map +1 -0
- package/lib/documents/hydranium-text-documents.d.ts +324 -57
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +590 -114
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/index.d.ts +3 -0
- package/lib/documents/index.d.ts.map +1 -1
- package/lib/documents/index.js +3 -0
- package/lib/documents/index.js.map +1 -1
- package/lib/documents/language-client-text-shadow.d.ts +41 -9
- package/lib/documents/language-client-text-shadow.d.ts.map +1 -1
- package/lib/documents/language-client-text-shadow.js +56 -11
- package/lib/documents/language-client-text-shadow.js.map +1 -1
- package/lib/documents/self-save-registry.d.ts +37 -6
- package/lib/documents/self-save-registry.d.ts.map +1 -1
- package/lib/documents/self-save-registry.js +28 -15
- package/lib/documents/self-save-registry.js.map +1 -1
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +6 -0
- package/lib/index.js.map +1 -1
- package/lib/langium/ast-extension/ast-node-builder.d.ts +32 -9
- package/lib/langium/ast-extension/ast-node-builder.d.ts.map +1 -1
- package/lib/langium/ast-extension/ast-node-builder.js +60 -22
- package/lib/langium/ast-extension/ast-node-builder.js.map +1 -1
- package/lib/langium/bootstrap.d.ts.map +1 -1
- package/lib/langium/bootstrap.js +16 -9
- package/lib/langium/bootstrap.js.map +1 -1
- package/lib/langium/config/configuration-provider.d.ts +81 -0
- package/lib/langium/config/configuration-provider.d.ts.map +1 -0
- package/lib/langium/config/configuration-provider.js +112 -0
- package/lib/langium/config/configuration-provider.js.map +1 -0
- package/lib/langium/config/index.d.ts +1 -0
- package/lib/langium/config/index.d.ts.map +1 -1
- package/lib/langium/config/index.js +1 -0
- package/lib/langium/config/index.js.map +1 -1
- package/lib/langium/diagnostics/lsp-logger.d.ts +17 -0
- package/lib/langium/diagnostics/lsp-logger.d.ts.map +1 -1
- package/lib/langium/diagnostics/lsp-logger.js +40 -6
- package/lib/langium/diagnostics/lsp-logger.js.map +1 -1
- package/lib/langium/document-builder/build-pipeline-integration.d.ts +21 -1
- package/lib/langium/document-builder/build-pipeline-integration.d.ts.map +1 -1
- package/lib/langium/document-builder/build-pipeline-integration.js +32 -5
- package/lib/langium/document-builder/build-pipeline-integration.js.map +1 -1
- package/lib/langium/document-builder/build-session.d.ts +93 -0
- package/lib/langium/document-builder/build-session.d.ts.map +1 -0
- package/lib/langium/document-builder/build-session.js +72 -0
- package/lib/langium/document-builder/build-session.js.map +1 -0
- package/lib/langium/document-builder/document-builder.d.ts +165 -9
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +313 -13
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/document-builder/index.d.ts +1 -0
- package/lib/langium/document-builder/index.d.ts.map +1 -1
- package/lib/langium/document-builder/index.js +1 -0
- package/lib/langium/document-builder/index.js.map +1 -1
- package/lib/langium/integration-services.d.ts +45 -1
- package/lib/langium/integration-services.d.ts.map +1 -1
- package/lib/langium/integration-services.js +10 -5
- package/lib/langium/integration-services.js.map +1 -1
- package/lib/langium/integrity/integrity-rule.d.ts +13 -5
- package/lib/langium/integrity/integrity-rule.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-rule.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +54 -6
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +192 -17
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/keys/containment.d.ts +64 -0
- package/lib/langium/keys/containment.d.ts.map +1 -0
- package/lib/langium/keys/containment.js +53 -0
- package/lib/langium/keys/containment.js.map +1 -0
- package/lib/langium/keys/index.d.ts +1 -0
- package/lib/langium/keys/index.d.ts.map +1 -1
- package/lib/langium/keys/index.js +1 -0
- package/lib/langium/keys/index.js.map +1 -1
- package/lib/langium/keys/name-based-key-provider.d.ts +4 -0
- package/lib/langium/keys/name-based-key-provider.d.ts.map +1 -1
- package/lib/langium/keys/name-based-key-provider.js +4 -0
- package/lib/langium/keys/name-based-key-provider.js.map +1 -1
- package/lib/langium/language-module.d.ts +27 -2
- package/lib/langium/language-module.d.ts.map +1 -1
- package/lib/langium/language-module.js +16 -2
- package/lib/langium/language-module.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts +299 -0
- package/lib/langium/model-service/client-session.d.ts.map +1 -0
- package/lib/langium/model-service/client-session.js +379 -0
- package/lib/langium/model-service/client-session.js.map +1 -0
- package/lib/langium/model-service/index.d.ts +1 -0
- package/lib/langium/model-service/index.d.ts.map +1 -1
- package/lib/langium/model-service/index.js +1 -0
- package/lib/langium/model-service/index.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +224 -160
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +191 -288
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +95 -16
- package/lib/langium/module.d.ts.map +1 -1
- package/lib/langium/module.js +30 -12
- package/lib/langium/module.js.map +1 -1
- package/lib/langium/naming/name-provider.d.ts +25 -4
- package/lib/langium/naming/name-provider.d.ts.map +1 -1
- package/lib/langium/naming/name-provider.js +2 -1
- package/lib/langium/naming/name-provider.js.map +1 -1
- package/lib/langium/naming/name-separator-validation.d.ts +23 -0
- package/lib/langium/naming/name-separator-validation.d.ts.map +1 -1
- package/lib/langium/naming/name-separator-validation.js +30 -1
- package/lib/langium/naming/name-separator-validation.js.map +1 -1
- package/lib/langium/residency/cst-residency-service.d.ts +31 -11
- package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
- package/lib/langium/residency/cst-residency-service.js +13 -56
- package/lib/langium/residency/cst-residency-service.js.map +1 -1
- package/lib/langium/scope/hydranium-scope-provider.d.ts +48 -13
- package/lib/langium/scope/hydranium-scope-provider.d.ts.map +1 -1
- package/lib/langium/scope/hydranium-scope-provider.js +56 -20
- package/lib/langium/scope/hydranium-scope-provider.js.map +1 -1
- package/lib/langium/serialization/abstract-serializer.d.ts +11 -20
- package/lib/langium/serialization/abstract-serializer.d.ts.map +1 -1
- package/lib/langium/serialization/abstract-serializer.js +12 -21
- package/lib/langium/serialization/abstract-serializer.js.map +1 -1
- package/lib/langium/service-registry.d.ts +15 -1
- package/lib/langium/service-registry.d.ts.map +1 -1
- package/lib/langium/service-registry.js +16 -0
- package/lib/langium/service-registry.js.map +1 -1
- package/lib/langium/shared-services.d.ts +15 -2
- package/lib/langium/shared-services.d.ts.map +1 -1
- package/lib/langium/shared-services.js.map +1 -1
- package/lib/langium/transfer/transfer-encoder.d.ts +60 -17
- package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
- package/lib/langium/transfer/transfer-encoder.js +38 -16
- package/lib/langium/transfer/transfer-encoder.js.map +1 -1
- package/lib/langium/trivia/comment-preserver.d.ts +423 -0
- package/lib/langium/trivia/comment-preserver.d.ts.map +1 -0
- package/lib/langium/trivia/comment-preserver.js +906 -0
- package/lib/langium/trivia/comment-preserver.js.map +1 -0
- package/lib/langium/trivia/document-ending-preserver.d.ts +43 -0
- package/lib/langium/trivia/document-ending-preserver.d.ts.map +1 -0
- package/lib/langium/trivia/document-ending-preserver.js +48 -0
- package/lib/langium/trivia/document-ending-preserver.js.map +1 -0
- package/lib/langium/trivia/index.d.ts +14 -0
- package/lib/langium/trivia/index.d.ts.map +1 -0
- package/lib/langium/trivia/index.js +14 -0
- package/lib/langium/trivia/index.js.map +1 -0
- package/lib/langium/trivia/trivia-contribution.d.ts +37 -0
- package/lib/langium/trivia/trivia-contribution.d.ts.map +1 -0
- package/lib/langium/trivia/trivia-contribution.js +10 -0
- package/lib/langium/trivia/trivia-contribution.js.map +1 -0
- package/lib/langium/trivia/trivia-preserver.d.ts +50 -0
- package/lib/langium/trivia/trivia-preserver.d.ts.map +1 -0
- package/lib/langium/trivia/trivia-preserver.js +10 -0
- package/lib/langium/trivia/trivia-preserver.js.map +1 -0
- package/lib/langium/trivia/trivia-service.d.ts +70 -0
- package/lib/langium/trivia/trivia-service.d.ts.map +1 -0
- package/lib/langium/trivia/trivia-service.js +56 -0
- package/lib/langium/trivia/trivia-service.js.map +1 -0
- package/lib/langium/update-rewrite/update-rewrite.d.ts +1 -1
- package/lib/langium/validation/document-validator.d.ts +294 -10
- package/lib/langium/validation/document-validator.d.ts.map +1 -1
- package/lib/langium/validation/document-validator.js +448 -5
- package/lib/langium/validation/document-validator.js.map +1 -1
- package/lib/langium/workspace/document-uri-policy.d.ts +3 -4
- package/lib/langium/workspace/document-uri-policy.d.ts.map +1 -1
- package/lib/langium/workspace/document-uri-policy.js +3 -4
- package/lib/langium/workspace/document-uri-policy.js.map +1 -1
- package/lib/langium/workspace/file-system-provider.d.ts +9 -1
- package/lib/langium/workspace/file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/file-system-provider.js +40 -7
- package/lib/langium/workspace/file-system-provider.js.map +1 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.d.ts +25 -0
- package/lib/langium/workspace/hydranium-langium-document-factory.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.js +25 -0
- package/lib/langium/workspace/hydranium-langium-document-factory.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts +21 -1
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.js +28 -0
- package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts +23 -0
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.js +55 -9
- package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
- package/lib/langium/workspace/initialize-workspace.d.ts +31 -2
- package/lib/langium/workspace/initialize-workspace.d.ts.map +1 -1
- package/lib/langium/workspace/initialize-workspace.js +13 -5
- package/lib/langium/workspace/initialize-workspace.js.map +1 -1
- package/lib/langium/workspace/langium-documents.d.ts +79 -17
- package/lib/langium/workspace/langium-documents.d.ts.map +1 -1
- package/lib/langium/workspace/langium-documents.js +83 -28
- package/lib/langium/workspace/langium-documents.js.map +1 -1
- package/lib/langium/workspace/virtual-document.d.ts +18 -3
- package/lib/langium/workspace/virtual-document.d.ts.map +1 -1
- package/lib/langium/workspace/virtual-document.js +19 -2
- package/lib/langium/workspace/virtual-document.js.map +1 -1
- package/lib/langium/workspace/write-lock-scope.d.ts +1 -1
- package/lib/langium/workspace/write-lock-scope.js +1 -1
- package/lib/locale/index.d.ts +10 -0
- package/lib/locale/index.d.ts.map +1 -0
- package/lib/locale/index.js +10 -0
- package/lib/locale/index.js.map +1 -0
- package/lib/locale/server-locale.d.ts +73 -0
- package/lib/locale/server-locale.d.ts.map +1 -0
- package/lib/locale/server-locale.js +61 -0
- package/lib/locale/server-locale.js.map +1 -0
- package/lib/lsp/completion/hydranium-completion-provider.d.ts +44 -2
- package/lib/lsp/completion/hydranium-completion-provider.d.ts.map +1 -1
- package/lib/lsp/completion/hydranium-completion-provider.js +68 -0
- package/lib/lsp/completion/hydranium-completion-provider.js.map +1 -1
- package/lib/lsp/connection-features.d.ts +23 -0
- package/lib/lsp/connection-features.d.ts.map +1 -0
- package/lib/lsp/connection-features.js +60 -0
- package/lib/lsp/connection-features.js.map +1 -0
- package/lib/lsp/diagnostics-connection.d.ts +27 -0
- package/lib/lsp/diagnostics-connection.d.ts.map +1 -0
- package/lib/lsp/diagnostics-connection.js +53 -0
- package/lib/lsp/diagnostics-connection.js.map +1 -0
- package/lib/lsp/hydranium-document-update-handler.d.ts +70 -62
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +123 -75
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/lsp/index.d.ts +3 -1
- package/lib/lsp/index.d.ts.map +1 -1
- package/lib/lsp/index.js +3 -1
- package/lib/lsp/index.js.map +1 -1
- package/lib/lsp/lsp-latency.d.ts +39 -0
- package/lib/lsp/lsp-latency.d.ts.map +1 -0
- package/lib/lsp/lsp-latency.js +58 -0
- package/lib/lsp/lsp-latency.js.map +1 -0
- package/lib/lsp/semantic-token-provider.d.ts +34 -1
- package/lib/lsp/semantic-token-provider.d.ts.map +1 -1
- package/lib/lsp/semantic-token-provider.js +55 -8
- package/lib/lsp/semantic-token-provider.js.map +1 -1
- package/lib/lsp/shared-module.d.ts +4 -8
- package/lib/lsp/shared-module.d.ts.map +1 -1
- package/lib/lsp/shared-module.js.map +1 -1
- package/lib/lsp/start-language-server.d.ts +8 -7
- package/lib/lsp/start-language-server.d.ts.map +1 -1
- package/lib/lsp/start-language-server.js +13 -7
- package/lib/lsp/start-language-server.js.map +1 -1
- package/lib/messages/carriers.d.ts +54 -0
- package/lib/messages/carriers.d.ts.map +1 -0
- package/lib/messages/carriers.js +62 -0
- package/lib/messages/carriers.js.map +1 -0
- package/lib/messages/index.d.ts +25 -0
- package/lib/messages/index.d.ts.map +1 -0
- package/lib/messages/index.js +25 -0
- package/lib/messages/index.js.map +1 -0
- package/lib/messages/renderer.d.ts +142 -0
- package/lib/messages/renderer.d.ts.map +1 -0
- package/lib/messages/renderer.js +165 -0
- package/lib/messages/renderer.js.map +1 -0
- package/lib/node/heap-ceiling.d.ts +61 -0
- package/lib/node/heap-ceiling.d.ts.map +1 -0
- package/lib/node/heap-ceiling.js +72 -0
- package/lib/node/heap-ceiling.js.map +1 -0
- package/lib/node/index.d.ts +1 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +1 -0
- package/lib/node/index.js.map +1 -1
- package/lib/node/latency-from-env.d.ts +1 -1
- package/lib/node/latency-from-env.js +1 -1
- package/lib/node/measure-memory.d.ts.map +1 -1
- package/lib/node/measure-memory.js +1 -0
- package/lib/node/measure-memory.js.map +1 -1
- package/lib/node/node-file-system-provider.d.ts +35 -1
- package/lib/node/node-file-system-provider.d.ts.map +1 -1
- package/lib/node/node-file-system-provider.js +140 -45
- package/lib/node/node-file-system-provider.js.map +1 -1
- package/lib/node/profile-capture.d.ts +8 -2
- package/lib/node/profile-capture.d.ts.map +1 -1
- package/lib/node/profile-capture.js.map +1 -1
- package/lib/node/profiling-run.d.ts +2 -2
- package/lib/node/profiling-run.js +1 -1
- package/lib/node/profiling-run.js.map +1 -1
- package/lib/node/rename-over-open-readers.d.ts +52 -0
- package/lib/node/rename-over-open-readers.d.ts.map +1 -0
- package/lib/node/rename-over-open-readers.js +66 -0
- package/lib/node/rename-over-open-readers.js.map +1 -0
- package/lib/node/socket-launcher.d.ts +15 -0
- package/lib/node/socket-launcher.d.ts.map +1 -1
- package/lib/node/socket-launcher.js +3 -2
- package/lib/node/socket-launcher.js.map +1 -1
- package/lib/testing/fake-document.d.ts +16 -4
- package/lib/testing/fake-document.d.ts.map +1 -1
- package/lib/testing/fake-document.js +18 -3
- package/lib/testing/fake-document.js.map +1 -1
- package/lib/testing/make-noop-shared-services.d.ts +14 -0
- package/lib/testing/make-noop-shared-services.d.ts.map +1 -1
- package/lib/testing/make-noop-shared-services.js +28 -2
- package/lib/testing/make-noop-shared-services.js.map +1 -1
- package/lib/testing/make-test-services.d.ts +61 -13
- package/lib/testing/make-test-services.d.ts.map +1 -1
- package/lib/testing/make-test-services.js +25 -5
- package/lib/testing/make-test-services.js.map +1 -1
- package/lib/testing/node/index.d.ts +1 -0
- package/lib/testing/node/index.d.ts.map +1 -1
- package/lib/testing/node/index.js +1 -0
- package/lib/testing/node/index.js.map +1 -1
- package/lib/testing/node/lsp-harness.d.ts.map +1 -1
- package/lib/testing/node/lsp-harness.js +2 -0
- package/lib/testing/node/lsp-harness.js.map +1 -1
- package/lib/testing/node/lsp-server-connection.d.ts +11 -3
- package/lib/testing/node/lsp-server-connection.d.ts.map +1 -1
- package/lib/testing/node/lsp-server-connection.js +42 -9
- package/lib/testing/node/lsp-server-connection.js.map +1 -1
- package/lib/testing/node/scratch-workspace.d.ts.map +1 -1
- package/lib/testing/node/scratch-workspace.js +10 -5
- package/lib/testing/node/scratch-workspace.js.map +1 -1
- package/lib/testing/node/spawned-server.d.ts +14 -0
- package/lib/testing/node/spawned-server.d.ts.map +1 -1
- package/lib/testing/node/spawned-server.js +33 -1
- package/lib/testing/node/spawned-server.js.map +1 -1
- package/lib/testing/node/unhandled-rejections.d.ts +20 -0
- package/lib/testing/node/unhandled-rejections.d.ts.map +1 -0
- package/lib/testing/node/unhandled-rejections.js +32 -0
- package/lib/testing/node/unhandled-rejections.js.map +1 -0
- package/lib/testing/playwright/flaky-network-proxy.d.ts +76 -0
- package/lib/testing/playwright/flaky-network-proxy.d.ts.map +1 -0
- package/lib/testing/playwright/flaky-network-proxy.js +164 -0
- package/lib/testing/playwright/flaky-network-proxy.js.map +1 -0
- package/lib/testing/playwright/index.d.ts +1 -0
- package/lib/testing/playwright/index.d.ts.map +1 -1
- package/lib/testing/playwright/index.js +1 -0
- package/lib/testing/playwright/index.js.map +1 -1
- package/lib/testing/playwright/server-log-capture.d.ts +14 -4
- package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
- package/lib/testing/playwright/server-log-capture.js +21 -4
- package/lib/testing/playwright/server-log-capture.js.map +1 -1
- package/lib/testing/playwright/server-log-rename-reporter.d.ts.map +1 -1
- package/lib/testing/playwright/server-log-rename-reporter.js +10 -1
- package/lib/testing/playwright/server-log-rename-reporter.js.map +1 -1
- package/lib/testing/run-update-pipeline.d.ts +4 -4
- package/lib/testing/run-update-pipeline.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +10 -9
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +8 -9
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-document-builder.d.ts +8 -0
- package/lib/testing/stub-document-builder.d.ts.map +1 -1
- package/lib/testing/stub-document-builder.js +22 -3
- package/lib/testing/stub-document-builder.js.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.d.ts +19 -6
- package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.js +53 -15
- package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
- package/lib/testing/stub-langium-documents.d.ts +3 -2
- package/lib/testing/stub-langium-documents.d.ts.map +1 -1
- package/lib/testing/stub-langium-documents.js.map +1 -1
- package/lib/testing/stub-model-service.d.ts +7 -6
- package/lib/testing/stub-model-service.d.ts.map +1 -1
- package/lib/testing/stub-model-service.js +4 -4
- package/lib/testing/stub-model-service.js.map +1 -1
- package/lib/util/connection-liveness.d.ts +7 -6
- package/lib/util/connection-liveness.d.ts.map +1 -1
- package/lib/util/connection-liveness.js +35 -14
- package/lib/util/connection-liveness.js.map +1 -1
- package/package.json +17 -8
- package/src/documents/ast-document-manager.ts +188 -45
- package/src/documents/client-ids.ts +5 -22
- package/src/documents/client-session-errors.ts +22 -0
- package/src/documents/client-session-registry.ts +237 -0
- package/src/documents/file-system-task-queue.ts +68 -0
- package/src/documents/hydranium-text-documents.ts +724 -142
- package/src/documents/index.ts +3 -0
- package/src/documents/language-client-text-shadow.ts +59 -11
- package/src/documents/self-save-registry.ts +52 -13
- package/src/index.ts +6 -0
- package/src/langium/ast-extension/ast-node-builder.ts +66 -22
- package/src/langium/bootstrap.ts +16 -9
- package/src/langium/config/configuration-provider.ts +119 -0
- package/src/langium/config/index.ts +1 -0
- package/src/langium/diagnostics/lsp-logger.ts +41 -6
- package/src/langium/document-builder/build-pipeline-integration.ts +50 -5
- package/src/langium/document-builder/build-session.ts +87 -0
- package/src/langium/document-builder/document-builder.ts +355 -13
- package/src/langium/document-builder/index.ts +1 -0
- package/src/langium/integration-services.ts +67 -6
- package/src/langium/integrity/integrity-rule.ts +13 -5
- package/src/langium/integrity/integrity-service.ts +201 -17
- package/src/langium/keys/containment.ts +87 -0
- package/src/langium/keys/index.ts +1 -0
- package/src/langium/keys/name-based-key-provider.ts +4 -0
- package/src/langium/language-module.ts +42 -4
- package/src/langium/model-service/client-session.ts +581 -0
- package/src/langium/model-service/index.ts +1 -0
- package/src/langium/model-service/model-service.ts +332 -274
- package/src/langium/module.ts +119 -22
- package/src/langium/naming/name-provider.ts +27 -5
- package/src/langium/naming/name-separator-validation.ts +34 -5
- package/src/langium/residency/cst-residency-service.ts +36 -14
- package/src/langium/scope/hydranium-scope-provider.ts +60 -22
- package/src/langium/serialization/abstract-serializer.ts +13 -32
- package/src/langium/service-registry.ts +19 -2
- package/src/langium/shared-services.ts +19 -2
- package/src/langium/transfer/transfer-encoder.ts +85 -24
- package/src/langium/trivia/comment-preserver.ts +1100 -0
- package/src/langium/trivia/document-ending-preserver.ts +55 -0
- package/src/langium/trivia/index.ts +14 -0
- package/src/langium/trivia/trivia-contribution.ts +39 -0
- package/src/langium/trivia/trivia-preserver.ts +54 -0
- package/src/langium/trivia/trivia-service.ts +99 -0
- package/src/langium/update-rewrite/update-rewrite.ts +1 -1
- package/src/langium/validation/document-validator.ts +537 -11
- package/src/langium/workspace/document-uri-policy.ts +3 -4
- package/src/langium/workspace/file-system-provider.ts +48 -8
- package/src/langium/workspace/hydranium-langium-document-factory.ts +34 -0
- package/src/langium/workspace/hydranium-workspace-manager.ts +30 -1
- package/src/langium/workspace/in-memory-file-system-provider.ts +62 -10
- package/src/langium/workspace/initialize-workspace.ts +43 -5
- package/src/langium/workspace/langium-documents.ts +106 -33
- package/src/langium/workspace/virtual-document.ts +21 -3
- package/src/langium/workspace/write-lock-scope.ts +1 -1
- package/src/locale/index.ts +10 -0
- package/src/locale/server-locale.ts +86 -0
- package/src/lsp/completion/hydranium-completion-provider.ts +82 -2
- package/src/lsp/connection-features.ts +67 -0
- package/src/lsp/diagnostics-connection.ts +56 -0
- package/src/lsp/hydranium-document-update-handler.ts +152 -79
- package/src/lsp/index.ts +3 -1
- package/src/lsp/lsp-latency.ts +62 -0
- package/src/lsp/semantic-token-provider.ts +74 -7
- package/src/lsp/shared-module.ts +4 -8
- package/src/lsp/start-language-server.ts +13 -7
- package/src/messages/carriers.ts +78 -0
- package/src/messages/index.ts +36 -0
- package/src/messages/renderer.ts +212 -0
- package/src/node/heap-ceiling.ts +108 -0
- package/src/node/index.ts +1 -0
- package/src/node/latency-from-env.ts +1 -1
- package/src/node/measure-memory.ts +1 -0
- package/src/node/node-file-system-provider.ts +154 -47
- package/src/node/profile-capture.ts +8 -2
- package/src/node/profiling-run.ts +3 -3
- package/src/node/rename-over-open-readers.ts +78 -0
- package/src/node/socket-launcher.ts +18 -2
- package/src/testing/fake-document.ts +24 -5
- package/src/testing/make-noop-shared-services.ts +50 -2
- package/src/testing/make-test-services.ts +99 -24
- package/src/testing/node/index.ts +1 -0
- package/src/testing/node/lsp-harness.ts +2 -0
- package/src/testing/node/lsp-server-connection.ts +61 -11
- package/src/testing/node/scratch-workspace.ts +10 -5
- package/src/testing/node/spawned-server.ts +35 -1
- package/src/testing/node/unhandled-rejections.ts +31 -0
- package/src/testing/playwright/flaky-network-proxy.ts +240 -0
- package/src/testing/playwright/index.ts +1 -0
- package/src/testing/playwright/server-log-capture.ts +24 -5
- package/src/testing/playwright/server-log-rename-reporter.ts +12 -1
- package/src/testing/run-update-pipeline.ts +4 -4
- package/src/testing/stub-ast-document-manager.ts +18 -31
- package/src/testing/stub-document-builder.ts +32 -3
- package/src/testing/stub-hydranium-text-documents.ts +78 -21
- package/src/testing/stub-langium-documents.ts +3 -2
- package/src/testing/stub-model-service.ts +9 -8
- package/src/util/connection-liveness.ts +37 -14
- package/lib/lsp/instrument-connection.d.ts +0 -36
- package/lib/lsp/instrument-connection.d.ts.map +0 -1
- package/lib/lsp/instrument-connection.js +0 -62
- package/lib/lsp/instrument-connection.js.map +0 -1
- package/src/lsp/instrument-connection.ts +0 -66
|
@@ -18,11 +18,15 @@
|
|
|
18
18
|
// on lsp-server, contradicting the peer architecture.
|
|
19
19
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
20
20
|
import { NormalizedTextDocuments } from '@hydranium/langium/lsp';
|
|
21
|
-
import { UriUtils } from '@hydranium/langium';
|
|
22
|
-
import { OptionalVersionedTextDocumentIdentifier, TextDocumentEdit, TextDocumentSyncKind } from 'vscode-languageserver';
|
|
21
|
+
import { URI, UriUtils } from '@hydranium/langium';
|
|
22
|
+
import { Emitter, OptionalVersionedTextDocumentIdentifier, TextDocumentContentChangeEvent as ContentChange, TextDocumentEdit, TextDocumentSyncKind } from 'vscode-languageserver';
|
|
23
23
|
import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
24
24
|
import { asLanguageClientUri, DisposableCollection } from '@hydranium/protocol';
|
|
25
|
+
import { HYDRANIUM_BUILD_REASONS } from '../langium/document-builder/document-builder.js';
|
|
26
|
+
import { isConnectionGoneError } from '../util/connection-liveness.js';
|
|
25
27
|
import { LANGUAGE_CLIENT_ID } from './client-ids.js';
|
|
28
|
+
import { INTEGRITY_CLIENT_ID } from '../langium/integrity/integrity-rule.js';
|
|
29
|
+
import { ClientSessionRegistry } from './client-session-registry.js';
|
|
26
30
|
import { isFullReplace, LanguageClientTextShadow } from './language-client-text-shadow.js';
|
|
27
31
|
/**
|
|
28
32
|
* The LSP spec's "version is intentionally unknown" for an
|
|
@@ -32,6 +36,12 @@ import { isFullReplace, LanguageClientTextShadow } from './language-client-text-
|
|
|
32
36
|
* cast at each call site.
|
|
33
37
|
*/
|
|
34
38
|
const UNKNOWN_CLIENT_VERSION = null;
|
|
39
|
+
/**
|
|
40
|
+
* The default of {@link HydraniumTextDocumentsOptions.revertGraceMs}: long
|
|
41
|
+
* enough for a client whose connection dropped to register again and reopen
|
|
42
|
+
* its documents, which a data client does as soon as it has a connection.
|
|
43
|
+
*/
|
|
44
|
+
const DEFAULT_REVERT_GRACE_MS = 10_000;
|
|
35
45
|
/**
|
|
36
46
|
* Upper bound on {@link HydraniumTextDocuments.__pendingPushes} entries
|
|
37
47
|
* per URI. Echoes normally return within milliseconds and consume their
|
|
@@ -39,6 +49,17 @@ const UNKNOWN_CLIENT_VERSION = null;
|
|
|
39
49
|
* of leaking.
|
|
40
50
|
*/
|
|
41
51
|
const PENDING_ECHO_CAP = 32;
|
|
52
|
+
/**
|
|
53
|
+
* Whether `err` reports that the file of `target` does not exist, as a Node
|
|
54
|
+
* file system and the framework's providers do: code `ENOENT`, with `target`'s
|
|
55
|
+
* path. A missing file of another document is a different failure.
|
|
56
|
+
*/
|
|
57
|
+
function isFileNotFound(err, target) {
|
|
58
|
+
if (typeof err !== 'object' || err === null || !('code' in err) || err.code !== 'ENOENT') {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return !('path' in err) || err.path === target.fsPath;
|
|
62
|
+
}
|
|
42
63
|
/**
|
|
43
64
|
* Cheap, stable, non-cryptographic content hash (cyrb53) for the version
|
|
44
65
|
* sequence's "did the content change across close/reopen?" question. Only
|
|
@@ -65,15 +86,19 @@ function contentHash(text) {
|
|
|
65
86
|
* Multi-client text-document tracking on top of Langium's `NormalizedTextDocuments`.
|
|
66
87
|
*
|
|
67
88
|
* 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
|
|
89
|
+
* - Per-document client membership (multiple clients can attach to the same
|
|
90
|
+
* URI) and the client-session table, both kept by {@link __sessions}.
|
|
69
91
|
* - A SERVER-OWNED shared version sequence: per-URI, monotonic across
|
|
70
92
|
* close/reopen cycles, advancing exactly when the synced content changes.
|
|
71
93
|
* Client-declared version ids (Monaco's buffer numbering) feed only a
|
|
72
94
|
* per-client staleness guard and never leak into the shared sequence —
|
|
73
95
|
* the two are different things (an editor's edit-operation counter vs the
|
|
74
96
|
* document's content-revision number), and splicing them lets versions drift
|
|
75
|
-
* silently past
|
|
97
|
+
* silently past based-on gate holders.
|
|
76
98
|
* - Version-author history so each edit is attributable to its originating client.
|
|
99
|
+
* - The revert to disk once no client has a document open, for every head
|
|
100
|
+
* ({@link revertToDisk}), deferred by
|
|
101
|
+
* {@link HydraniumTextDocumentsOptions.revertGraceMs} after a lost connection.
|
|
77
102
|
* - Pending-content staging used by the integrity service to thread corrections
|
|
78
103
|
* through `workspace/applyEdit` cycles for currently-closed documents.
|
|
79
104
|
* - `didOpen` notifications arriving over the LSP connection wait on the
|
|
@@ -84,22 +109,26 @@ function contentHash(text) {
|
|
|
84
109
|
export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
85
110
|
services;
|
|
86
111
|
/**
|
|
87
|
-
* Per-URI client-facing tracking ({@link
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* {@link __shadow} (its own abstraction); the parsed-document store stays in
|
|
92
|
-
* the inherited `__syncedDocuments`.
|
|
112
|
+
* Per-URI client-facing tracking ({@link DocumentTrackingRecord}), keyed by
|
|
113
|
+
* canonical URI. One record per URI, so dropping it clears every axis at
|
|
114
|
+
* once. Which client has the document open is kept apart, in
|
|
115
|
+
* {@link __sessions}.
|
|
93
116
|
*/
|
|
94
117
|
__documents = new Map();
|
|
118
|
+
/**
|
|
119
|
+
* Which client has which document open, and which client ids are registered
|
|
120
|
+
* sessions. Every open-state predicate on this class reads it, so an open
|
|
121
|
+
* recorded anywhere else is invisible to the last-close transition.
|
|
122
|
+
*/
|
|
123
|
+
__sessions = new ClientSessionRegistry();
|
|
95
124
|
/**
|
|
96
125
|
* Per-URI shared-version continuity across close/reopen cycles
|
|
97
126
|
* ({@link VersionSequence}), written at last-client close and consulted by
|
|
98
|
-
* the next first-client open. DELIBERATELY outside {@link
|
|
127
|
+
* the next first-client open. DELIBERATELY outside {@link DocumentTrackingRecord}:
|
|
99
128
|
* that record is deleted on last close, while the version sequence must
|
|
100
129
|
* survive it — the shared version is a server-owned, monotonic,
|
|
101
130
|
* advances-iff-content-changes counter that never resets while the server
|
|
102
|
-
* lives. That invariant is what makes an optimistic
|
|
131
|
+
* lives. That invariant is what makes an optimistic based-on gate
|
|
103
132
|
* sound: "version unchanged ⇔ content unchanged", with no false conflicts
|
|
104
133
|
* from close/reopen version resets and no false passes from a reopened
|
|
105
134
|
* sequence coincidentally landing on a stale writer's number.
|
|
@@ -111,7 +140,7 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
111
140
|
/**
|
|
112
141
|
* Texts pushed to the LSP textual language client via
|
|
113
142
|
* {@link applyEditToLanguageClient} whose echoes have not come back yet
|
|
114
|
-
* ({@link
|
|
143
|
+
* ({@link PendingLanguageClientPush}), keyed like the shadow by language-client URI.
|
|
115
144
|
* Outbound pushes and inbound echoes are uncorrelated on the wire; this
|
|
116
145
|
* FIFO is the explicit correlation, and it is what
|
|
117
146
|
* {@link classifyLanguageClientChange} reconstructs against.
|
|
@@ -156,6 +185,10 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
156
185
|
__shadow;
|
|
157
186
|
tracer;
|
|
158
187
|
configuration;
|
|
188
|
+
/** See {@link HydraniumTextDocumentsOptions.revertGraceMs}. */
|
|
189
|
+
revertGraceMs;
|
|
190
|
+
lastOpenClosedEmitter = new Emitter();
|
|
191
|
+
languageClientSavedEmitter = new Emitter();
|
|
159
192
|
constructor(services, options = {}) {
|
|
160
193
|
const configuration = options.configuration ?? TextDocument;
|
|
161
194
|
super(configuration);
|
|
@@ -163,6 +196,8 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
163
196
|
this.configuration = configuration;
|
|
164
197
|
this.tracer = services.Tracer.for(options.logName ?? 'TextDocuments').trace('instantiated');
|
|
165
198
|
this.__shadow = new LanguageClientTextShadow((uri, reason) => this.tracer.with(uri).warn(`Diff apply-verify fallback (${reason}) — using full-document replace`), this);
|
|
199
|
+
this.revertGraceMs = options.revertGraceMs ?? DEFAULT_REVERT_GRACE_MS;
|
|
200
|
+
this.onDidCloseLastOpen(event => void this.revertToDisk(event.uri));
|
|
166
201
|
}
|
|
167
202
|
// Re-exposed configuration factories — for framework-internal callers
|
|
168
203
|
// (LanguageClientTextShadow's apply-verify probe; IntegrityService.resyncDocument)
|
|
@@ -235,7 +270,7 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
235
270
|
}));
|
|
236
271
|
disposables.push(connection.onWillSaveTextDocumentWaitUntil((event, token) => this.notifyWillSaveTextDocumentWaitUntil(event, token)));
|
|
237
272
|
disposables.push(connection.onDidSaveTextDocument((event) => {
|
|
238
|
-
this.
|
|
273
|
+
void this.notifyLanguageClientSave(event);
|
|
239
274
|
}));
|
|
240
275
|
return disposables;
|
|
241
276
|
}
|
|
@@ -267,18 +302,19 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
267
302
|
// Distinguish "already at this version" (common: an echo from the client that triggered
|
|
268
303
|
// the update) from "incoming version older than ours" (stale race).
|
|
269
304
|
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');
|
|
305
|
+
this.logUri(uri, `Ignore update by ${this.formatClientId(clientId)}: ${reason}`, 'debug');
|
|
271
306
|
return;
|
|
272
307
|
}
|
|
273
308
|
record.clientVersions.set(clientId, td.version);
|
|
274
|
-
// A language-client change
|
|
275
|
-
//
|
|
276
|
-
//
|
|
309
|
+
// A language-client change is keyed to the buffer that client holds,
|
|
310
|
+
// which is the synced text only while the two agree — an authored
|
|
311
|
+
// write advances the synced text without the client knowing, and a
|
|
312
|
+
// push in flight moves the client without the store knowing.
|
|
277
313
|
// Resolve which it is, and against what, before touching anything.
|
|
278
|
-
const
|
|
314
|
+
const origin = clientId === LANGUAGE_CLIENT_ID
|
|
279
315
|
? this.classifyLanguageClientChange(this.toLanguageClientUri(td.uri), document, changes)
|
|
280
316
|
: undefined;
|
|
281
|
-
if (
|
|
317
|
+
if (origin?.kind === 'echo') {
|
|
282
318
|
// The client is reporting a text we pushed it, so the synced
|
|
283
319
|
// document is ALREADY there and the changes must not be applied a
|
|
284
320
|
// second time. Nothing is minted and no rebuild fires. The
|
|
@@ -290,18 +326,22 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
290
326
|
this.logUri(uri, `Skip rebuild: echo of a server-authored push (client version ${td.version})`, 'debug');
|
|
291
327
|
return;
|
|
292
328
|
}
|
|
329
|
+
if (origin?.kind === 'unreconstructable') {
|
|
330
|
+
this.tracer.with(uri).warn(`Drop change: no known client buffer for its ranges (client version ${td.version})`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
293
333
|
// The SHARED version advances iff the content actually changes — the
|
|
294
|
-
// invariant optimistic
|
|
334
|
+
// invariant optimistic based-on gates rely on. The new text is
|
|
295
335
|
// only known after applying the (possibly incremental) changes, so
|
|
296
336
|
// apply at a tentative +1 and roll the version back on an identical
|
|
297
337
|
// result (an empty-changes update only re-stamps the version).
|
|
298
338
|
//
|
|
299
|
-
// A divergent
|
|
300
|
-
//
|
|
301
|
-
//
|
|
339
|
+
// A divergent change is applied as its RECONSTRUCTED text rather than
|
|
340
|
+
// as its own ranges: those ranges address the client's own buffer, so
|
|
341
|
+
// applying them here would splice the wrong lines.
|
|
302
342
|
const previousText = document.getText();
|
|
303
343
|
const sharedVersion = document.version;
|
|
304
|
-
document = this.configuration.update(document,
|
|
344
|
+
document = this.configuration.update(document, origin === undefined ? changes : [{ text: origin.text }], sharedVersion + 1);
|
|
305
345
|
const changed = document.getText() !== previousText;
|
|
306
346
|
if (!changed) {
|
|
307
347
|
document = this.configuration.update(document, [], sharedVersion);
|
|
@@ -325,7 +365,7 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
325
365
|
return;
|
|
326
366
|
}
|
|
327
367
|
}
|
|
328
|
-
this.log(document.uri, `Update to version ${document.version} by ${clientId}${changed ? '' : ' (content unchanged)'}`);
|
|
368
|
+
this.log(document.uri, `Update to version ${document.version} by ${this.formatClientId(clientId)}${changed ? '' : ' (content unchanged)'}`);
|
|
329
369
|
this.__onDidChangeContent.fire(Object.freeze({ document, clientId }));
|
|
330
370
|
}
|
|
331
371
|
}
|
|
@@ -341,7 +381,7 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
341
381
|
* A content-identical write still fires the change event (rebuild): the
|
|
342
382
|
* authored write's server-side rebuild is correctness-bearing, it just
|
|
343
383
|
* mints no new version — nothing observable changed, so watchers'
|
|
344
|
-
*
|
|
384
|
+
* based-on versions stay valid.
|
|
345
385
|
*
|
|
346
386
|
* Returns the resulting shared version. Throws when the document is not
|
|
347
387
|
* open — callers (`AstDocumentManager.update`) open first.
|
|
@@ -358,51 +398,183 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
358
398
|
this.__syncedDocuments.set(key, document);
|
|
359
399
|
this.setAuthor(key, document.version, clientId);
|
|
360
400
|
}
|
|
361
|
-
this.log(document.uri, `Update to version ${document.version} by ${clientId}${changed ? '' : ' (content unchanged)'}`);
|
|
401
|
+
this.log(document.uri, `Update to version ${document.version} by ${this.formatClientId(clientId)}${changed ? '' : ' (content unchanged)'}`);
|
|
362
402
|
this.__onDidChangeContent.fire(Object.freeze({ document, clientId }));
|
|
363
403
|
return document.version;
|
|
364
404
|
}
|
|
365
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Close `clientId`'s open of the document. When it was the last open, the
|
|
407
|
+
* document is released and reverts to disk, at once or, for a `'lost'`
|
|
408
|
+
* close, after {@link HydraniumTextDocumentsOptions.revertGraceMs}.
|
|
409
|
+
*/
|
|
410
|
+
notifyDidCloseTextDocument(event, clientId = LANGUAGE_CLIENT_ID, cause = 'closed') {
|
|
366
411
|
const uri = this.documentKey(event.textDocument.uri);
|
|
367
|
-
if (!this.
|
|
412
|
+
if (!this.__sessions.removeOpen(uri, clientId)) {
|
|
368
413
|
return;
|
|
369
414
|
}
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
415
|
+
const record = this.__documents.get(uri);
|
|
416
|
+
record?.clientVersions.delete(clientId);
|
|
417
|
+
if (record && cause === 'lost') {
|
|
418
|
+
(record.lostClients ??= new Map()).set(clientId, this.services.Clock.stopwatch());
|
|
419
|
+
}
|
|
373
420
|
const syncedDocument = this.__syncedDocuments.get(uri);
|
|
374
421
|
if (syncedDocument !== undefined) {
|
|
375
|
-
this.log(syncedDocument.uri, `Closed synced document: ${syncedDocument.version} by ${clientId}`);
|
|
422
|
+
this.log(syncedDocument.uri, `Closed synced document: ${syncedDocument.version} by ${this.formatClientId(clientId)}`);
|
|
376
423
|
this.__onDidClose.fire(Object.freeze({ document: syncedDocument, clientId }));
|
|
377
424
|
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
378
425
|
// Monaco closed the document; drop the URI it held and the shadow
|
|
379
426
|
// baselined under it. (If this was the last URI/client the whole record
|
|
380
|
-
// is deleted
|
|
427
|
+
// is deleted on release, clearing the set anyway.)
|
|
381
428
|
const droppedUri = this.toLanguageClientUri(event.textDocument.uri);
|
|
382
429
|
this.__documents.get(uri)?.languageClientUris?.delete(droppedUri);
|
|
383
430
|
this.__shadow.invalidate(droppedUri);
|
|
384
431
|
this.__pendingPushes.delete(droppedUri);
|
|
385
432
|
}
|
|
386
|
-
if (!this.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
433
|
+
if (!this.__sessions.isOpen(uri)) {
|
|
434
|
+
if (cause === 'lost' && this.revertGraceMs > 0) {
|
|
435
|
+
this.deferRelease(uri);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
this.releaseDocument(uri);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Keep the document, text and all, for the revert grace, then release it.
|
|
445
|
+
* The document stays in the store meanwhile, so a lost client that opens it
|
|
446
|
+
* again within its own grace attaches to it and finds its unsaved text
|
|
447
|
+
* rather than reading disk; see {@link resolvePendingRevert} for any other
|
|
448
|
+
* open.
|
|
449
|
+
*/
|
|
450
|
+
deferRelease(uri) {
|
|
451
|
+
this.log(uri, `No client left; revert deferred for ${this.revertGraceMs} ms (connection lost)`);
|
|
452
|
+
const timer = this.services.Clock.setTimer(() => {
|
|
453
|
+
this.__sessions.cancelRevert(uri);
|
|
454
|
+
if (!this.__sessions.isOpen(uri)) {
|
|
455
|
+
this.releaseDocument(uri);
|
|
456
|
+
}
|
|
457
|
+
}, this.revertGraceMs);
|
|
458
|
+
this.__sessions.deferRevert(uri, timer);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Resolve the pending revert of `uri` for an open by `clientId`. An open by a
|
|
462
|
+
* client lost from the document within its grace cancels the revert, and
|
|
463
|
+
* the client finds its unsaved text. Any other open releases the document
|
|
464
|
+
* first, so it opens as a first open does: cancelling for every open hands
|
|
465
|
+
* a lost client's unsaved text to whoever opens next, a reloaded page or an
|
|
466
|
+
* editor, with nothing marking it unsaved.
|
|
467
|
+
*/
|
|
468
|
+
resolvePendingRevert(uri, clientId) {
|
|
469
|
+
const record = this.__documents.get(uri);
|
|
470
|
+
if (record) {
|
|
471
|
+
this.pruneLostClients(record);
|
|
472
|
+
}
|
|
473
|
+
const returning = record?.lostClients?.delete(clientId) ?? false;
|
|
474
|
+
if (!this.__sessions.isRevertPending(uri)) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
this.__sessions.cancelRevert(uri);
|
|
478
|
+
if (!returning) {
|
|
479
|
+
this.releaseDocument(uri);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/** Drop the entries of {@link DocumentTrackingRecord.lostClients} whose grace has run out. */
|
|
483
|
+
pruneLostClients(record) {
|
|
484
|
+
for (const [clientId, sinceLoss] of record.lostClients ?? []) {
|
|
485
|
+
if (sinceLoss.elapsedMs >= this.revertGraceMs) {
|
|
486
|
+
record.lostClients?.delete(clientId);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Drop the document no client has open any more, then announce it on
|
|
492
|
+
* {@link onDidCloseLastOpen}, which is what reverts it to disk.
|
|
493
|
+
*/
|
|
494
|
+
releaseDocument(uri) {
|
|
495
|
+
const syncedDocument = this.__syncedDocuments.get(uri);
|
|
496
|
+
if (syncedDocument === undefined) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
this.log(syncedDocument.uri, `Remove synced document: ${syncedDocument.version} (no client left)`);
|
|
500
|
+
// Persist where the shared version sequence left off (version +
|
|
501
|
+
// content hash) so the next open CONTINUES the sequence instead of
|
|
502
|
+
// restarting at the reopening client's declared id. Hashed once
|
|
503
|
+
// here at release, not on every change.
|
|
504
|
+
this.__versionSequences.set(uri, {
|
|
505
|
+
version: syncedDocument.version,
|
|
506
|
+
contentHash: contentHash(syncedDocument.getText())
|
|
507
|
+
});
|
|
508
|
+
this.__syncedDocuments.delete(uri);
|
|
509
|
+
// One delete clears every per-URI axis (version history + any staged
|
|
510
|
+
// pending content) so a future open with the same URI starts fresh.
|
|
511
|
+
this.__documents.delete(uri);
|
|
512
|
+
this.lastOpenClosedEmitter.fire(Object.freeze({ uri }));
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Rebuild a released document from disk, so the build stops carrying the
|
|
516
|
+
* unsaved text of its last client.
|
|
517
|
+
*
|
|
518
|
+
* Only a `file:` document reverts. A document of another scheme may have
|
|
519
|
+
* been loaded by the adopter into the workspace index, and a rebuild would
|
|
520
|
+
* drop it and break every reference to it.
|
|
521
|
+
*
|
|
522
|
+
* The file's existence is read in its disk queue, so the rebuild follows
|
|
523
|
+
* any save still queued rather than reverting past it. A document with no
|
|
524
|
+
* file behind it — created and never saved, or deleted meanwhile — is
|
|
525
|
+
* removed from the workspace instead: there is no disk text to revert to,
|
|
526
|
+
* and one whose file goes after that read is removed when its rebuild
|
|
527
|
+
* finds none.
|
|
528
|
+
*
|
|
529
|
+
* Whether to revert at all is decided inside the write lock, as its holder:
|
|
530
|
+
* decided before waiting for the lock, a client that opens or re-creates the
|
|
531
|
+
* document meanwhile has its text rebuilt over from disk, or removed. A
|
|
532
|
+
* document some client has open again, or that waits out a new grace, is
|
|
533
|
+
* left to that client.
|
|
534
|
+
*/
|
|
535
|
+
async revertToDisk(uri) {
|
|
536
|
+
if (URI.parse(uri).scheme !== 'file') {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const workspace = this.services.workspace;
|
|
540
|
+
const target = UriUtils.toUri(uri);
|
|
541
|
+
const reopened = () => this.isOpenInAnyClient(uri) || this.__syncedDocuments.has(uri);
|
|
542
|
+
try {
|
|
543
|
+
// Read before the lock: every build and read waits while the lock is
|
|
544
|
+
// held, and this read waits on the file's save I/O.
|
|
545
|
+
const onDisk = await workspace.FileSystemTaskQueue.enqueue(uri, () => workspace.FileSystemProvider.exists(target));
|
|
546
|
+
await workspace.WorkspaceManager?.ready;
|
|
547
|
+
await workspace.WorkspaceLock.write(async (token) => {
|
|
548
|
+
if (reopened()) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
workspace.DocumentBuilder.markNextReason(HYDRANIUM_BUILD_REASONS.didClose);
|
|
552
|
+
try {
|
|
553
|
+
await (onDisk
|
|
554
|
+
? workspace.DocumentBuilder.update([target], [], token)
|
|
555
|
+
: workspace.DocumentBuilder.update([], [target], token));
|
|
556
|
+
}
|
|
557
|
+
catch (err) {
|
|
558
|
+
if (!onDisk || !isFileNotFound(err, target)) {
|
|
559
|
+
throw err;
|
|
560
|
+
}
|
|
561
|
+
// The file went after its existence was read.
|
|
562
|
+
this.tracer.with(uri).debug('Revert found no file; removing the document instead');
|
|
563
|
+
workspace.DocumentBuilder.markNextReason(HYDRANIUM_BUILD_REASONS.didClose);
|
|
564
|
+
await workspace.DocumentBuilder.update([], [target], token);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
// A revert that finishes after the LSP peer went away fails its
|
|
570
|
+
// diagnostics publish, and one that runs after its workspace was torn
|
|
571
|
+
// down finds no file: teardown races, not failed reverts.
|
|
572
|
+
if (isConnectionGoneError(err) || isFileNotFound(err, target)) {
|
|
573
|
+
this.tracer.with(uri).debug(`Revert on last close skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
574
|
+
return;
|
|
405
575
|
}
|
|
576
|
+
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
577
|
+
this.tracer.with(uri).error(`Revert on last close dropped. ${detail}`);
|
|
406
578
|
}
|
|
407
579
|
}
|
|
408
580
|
notifyWillSaveTextDocument(event) {
|
|
@@ -420,39 +592,91 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
420
592
|
return [];
|
|
421
593
|
}
|
|
422
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Handle a save the language client reports: fire
|
|
597
|
+
* {@link onDidSaveInLanguageClient}, then announce it on {@link onDidSave}
|
|
598
|
+
* only once the file read back from disk holds the store's current text.
|
|
599
|
+
*/
|
|
600
|
+
async notifyLanguageClientSave(event) {
|
|
601
|
+
const syncedDocument = this.__syncedDocuments.get(this.documentKey(event.textDocument.uri));
|
|
602
|
+
if (syncedDocument === undefined) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const uri = syncedDocument.uri;
|
|
606
|
+
this.languageClientSavedEmitter.fire(Object.freeze({ uri }));
|
|
607
|
+
// `onDidSave` tells its listeners that the shared document is on disk: a
|
|
608
|
+
// diagram marks its save done, the data head broadcasts a save and its
|
|
609
|
+
// clients drop their dirty state. The editor wrote its own buffer,
|
|
610
|
+
// which lags the store while another client's edit is still on its way
|
|
611
|
+
// to it, so only the file says whether that holds. Read it rather than
|
|
612
|
+
// trust the editor: `didSave` carries text only for a client that
|
|
613
|
+
// honours `includeText`, and asking for it means rewriting Langium's
|
|
614
|
+
// initialize answer, which advertises `save` as a Boolean. The read goes
|
|
615
|
+
// through the disk queue and waits on nothing but the provider, so it
|
|
616
|
+
// follows a server save queued meanwhile. A file changed again between
|
|
617
|
+
// the editor's write and this read compares unequal and takes the safe
|
|
618
|
+
// side. A save not announced leaves every other client dirty, which is
|
|
619
|
+
// true of the store's text: a diagram keeps its save pending, and data
|
|
620
|
+
// clients keep their unsaved state until the next save of the document.
|
|
621
|
+
let onDisk;
|
|
622
|
+
try {
|
|
623
|
+
onDisk = await this.services.workspace.FileSystemTaskQueue.enqueue(uri, () => this.services.workspace.FileSystemProvider.readFile(UriUtils.toUri(uri)));
|
|
624
|
+
}
|
|
625
|
+
catch (err) {
|
|
626
|
+
this.tracer
|
|
627
|
+
.with(uri)
|
|
628
|
+
.debug(`Editor save not announced: reading the file back failed. ${err instanceof Error ? err.message : String(err)}`);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
// An editor that saves and then closes releases the document while the
|
|
632
|
+
// read is under way; its save is still a save of the text it held.
|
|
633
|
+
const document = this.__syncedDocuments.get(this.documentKey(uri)) ?? syncedDocument;
|
|
634
|
+
if (onDisk !== document.getText()) {
|
|
635
|
+
this.tracer.with(uri).debug(`Editor save not announced: the file does not hold the text of version ${document.version}`);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
this.announceSave(document, LANGUAGE_CLIENT_ID);
|
|
639
|
+
}
|
|
423
640
|
notifyDidSaveTextDocument(event, clientId = LANGUAGE_CLIENT_ID) {
|
|
424
641
|
const syncedDocument = this.__syncedDocuments.get(this.documentKey(event.textDocument.uri));
|
|
425
642
|
if (syncedDocument !== undefined) {
|
|
426
|
-
this.
|
|
427
|
-
this.__onDidSave.fire(Object.freeze({ document: syncedDocument, clientId }));
|
|
643
|
+
this.announceSave(syncedDocument, clientId);
|
|
428
644
|
}
|
|
429
645
|
}
|
|
646
|
+
/** Fire {@link onDidSave} for `document`, which need not be synced any more. */
|
|
647
|
+
announceSave(document, clientId) {
|
|
648
|
+
this.log(document.uri, `Saved synced document: ${document.version} by ${this.formatClientId(clientId)}`);
|
|
649
|
+
this.__onDidSave.fire(Object.freeze({ document, clientId }));
|
|
650
|
+
}
|
|
430
651
|
notifyDidOpenTextDocument(event, clientId = LANGUAGE_CLIENT_ID) {
|
|
431
652
|
const td = event.textDocument;
|
|
432
653
|
const uri = this.documentKey(td.uri);
|
|
654
|
+
this.resolvePendingRevert(uri, clientId);
|
|
433
655
|
if (this.isOpenInClient(uri, clientId)) {
|
|
434
656
|
// Already open for this client under this canonical identity. If this is a
|
|
435
657
|
// 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
|
|
437
|
-
//
|
|
438
|
-
//
|
|
658
|
+
// divergent path, e.g. a symlink and its real path), record it so outbound
|
|
659
|
+
// edits reach this tab too — but do NOT re-fire open/rebuild; the document
|
|
660
|
+
// is already live.
|
|
439
661
|
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
440
662
|
const clientFacing = this.toLanguageClientUri(td.uri);
|
|
441
663
|
const record = this.__documents.get(uri);
|
|
442
664
|
if (record && !record.languageClientUris?.has(clientFacing)) {
|
|
443
665
|
(record.languageClientUris ??= new Set()).add(clientFacing);
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
666
|
+
// This tab's own buffer, NOT the synced text: a second tab is a second
|
|
667
|
+
// client model, read from disk, so a server-authored write already
|
|
668
|
+
// applied to the first tab leaves it BEHIND the synced document. Keying
|
|
669
|
+
// a diff to the synced text here addresses lines this tab does not have
|
|
670
|
+
// and drops content it does.
|
|
671
|
+
this.__shadow.setOpenedText(clientFacing, td.text);
|
|
448
672
|
}
|
|
449
673
|
}
|
|
450
674
|
return;
|
|
451
675
|
}
|
|
452
676
|
let document = this.__syncedDocuments.get(uri);
|
|
677
|
+
const existingClients = this.__sessions.clientsOf(uri);
|
|
678
|
+
this.__sessions.addOpen(uri, clientId);
|
|
453
679
|
const record = this.trackingFor(uri);
|
|
454
|
-
const existingClients = [...record.clients];
|
|
455
|
-
record.clients.add(clientId);
|
|
456
680
|
// Baseline the per-client staleness guard at the version id the client
|
|
457
681
|
// declared for its own buffer (client-owned per LSP).
|
|
458
682
|
record.clientVersions.set(clientId, td.version);
|
|
@@ -468,13 +692,13 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
468
692
|
const source = pendingText ? ', source=pending' : '';
|
|
469
693
|
// The SHARED version is server-assigned: continue the persisted
|
|
470
694
|
// sequence — same version when the content is unchanged since the
|
|
471
|
-
// last close (so watchers'
|
|
695
|
+
// last close (so watchers' based-on versions stay valid), one
|
|
472
696
|
// step when it changed (so no stale pointer can coincidentally pass
|
|
473
697
|
// the optimistic gate). Only a first-ever open adopts the client's
|
|
474
698
|
// declared id as the sequence seed.
|
|
475
699
|
const sequence = this.__versionSequences.get(uri);
|
|
476
700
|
const version = sequence === undefined ? td.version : sequence.contentHash === contentHash(text) ? sequence.version : sequence.version + 1;
|
|
477
|
-
this.log(uri, `Open document: Version ${version} by ${clientId} [first client${source}]`);
|
|
701
|
+
this.log(uri, `Open document: Version ${version} by ${this.formatClientId(clientId)} [first client${source}]`);
|
|
478
702
|
document = this.configuration.create(uri, td.languageId, version, text);
|
|
479
703
|
this.__syncedDocuments.set(uri, document);
|
|
480
704
|
this.setAuthor(uri, version, clientId);
|
|
@@ -482,7 +706,18 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
482
706
|
// Baseline the shadow to what Monaco just opened so the next outbound
|
|
483
707
|
// applyEditToLanguageClient diffs against the right starting point. Keyed by the
|
|
484
708
|
// language-client URI (what Monaco holds), not the canonical document key.
|
|
485
|
-
|
|
709
|
+
//
|
|
710
|
+
// The CLIENT's declared text, never the synced document: staged content
|
|
711
|
+
// consumed above leaves the two different, and a baseline asserting the
|
|
712
|
+
// client already holds the staged text suppresses the one sync that would
|
|
713
|
+
// deliver it. Skipped entirely when a push is already outstanding for this
|
|
714
|
+
// URI — an `applyEdit` to a closed file has the client open from disk and
|
|
715
|
+
// apply afterwards, so that shadow records what it is about to hold and
|
|
716
|
+
// disk text would key the next diff to a buffer nobody has.
|
|
717
|
+
const clientFacing = this.toLanguageClientUri(td.uri);
|
|
718
|
+
if (!this.__shadow.isTracked(clientFacing)) {
|
|
719
|
+
this.__shadow.set(clientFacing, td.text);
|
|
720
|
+
}
|
|
486
721
|
}
|
|
487
722
|
const toFire = Object.freeze({ document, clientId });
|
|
488
723
|
this.__onDidOpen.fire(toFire);
|
|
@@ -490,16 +725,64 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
490
725
|
}
|
|
491
726
|
else {
|
|
492
727
|
// An additional client attaches to a document already open by another client.
|
|
493
|
-
this.
|
|
494
|
-
|
|
728
|
+
this.logClientJoined(uri, clientId, document.version, existingClients);
|
|
729
|
+
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
730
|
+
// Monaco's own buffer, which on this path is NOT the synced text — another
|
|
731
|
+
// client opened the document and may already have changed it. Recorded as
|
|
732
|
+
// an equality-only baseline (never a diff one, see `setOpenedText`) so the
|
|
733
|
+
// refresh below does not push a full replace of content Monaco already
|
|
734
|
+
// holds, which would dirty the file on open.
|
|
735
|
+
this.__shadow.setOpenedText(this.toLanguageClientUri(td.uri), td.text);
|
|
736
|
+
}
|
|
495
737
|
this.refreshContent(uri, clientId);
|
|
496
738
|
}
|
|
497
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* Record `clientId` as an additional holder of a document another client
|
|
742
|
+
* already has open.
|
|
743
|
+
*
|
|
744
|
+
* Unlike the attach branch of {@link notifyDidOpenTextDocument} this does
|
|
745
|
+
* not refresh. That branch re-renders an attaching TEXTUAL view; a client
|
|
746
|
+
* reaching a document through a non-textual route has nothing to re-render,
|
|
747
|
+
* and refreshing per attach turns each into a Langium rebuild and dependent
|
|
748
|
+
* relink cascade.
|
|
749
|
+
*
|
|
750
|
+
* The staleness guard baselines at the SYNCED version, not a declared buffer
|
|
751
|
+
* version, because a client arriving this way holds no buffer of its own.
|
|
752
|
+
* Unifying the two routes therefore rebaselines a textual client's guard to a
|
|
753
|
+
* version it never declared.
|
|
754
|
+
*
|
|
755
|
+
* Returns whether a hold was added — `false` when `uri` is not open,
|
|
756
|
+
* `clientId` already holds it, or the document was waiting out the revert
|
|
757
|
+
* grace for other clients and was released instead (see
|
|
758
|
+
* {@link resolvePendingRevert}); the caller then opens it anew.
|
|
759
|
+
*/
|
|
760
|
+
attachClient(uri, clientId) {
|
|
761
|
+
const key = this.documentKey(uri);
|
|
762
|
+
if (!this.__syncedDocuments.has(key) || this.isOpenInClient(key, clientId)) {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
this.resolvePendingRevert(key, clientId);
|
|
766
|
+
const document = this.__syncedDocuments.get(key);
|
|
767
|
+
if (!document) {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
const existingClients = this.__sessions.clientsOf(key);
|
|
771
|
+
this.__sessions.addOpen(key, clientId);
|
|
772
|
+
const record = this.trackingFor(key);
|
|
773
|
+
record.clientVersions.set(clientId, document.version);
|
|
774
|
+
this.logClientJoined(key, clientId, document.version, existingClients);
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
logClientJoined(uri, clientId, version, existingClients) {
|
|
778
|
+
this.log(uri, `Attach client: ${this.formatClientId(clientId)} joined existing document (version ${version}, ` +
|
|
779
|
+
`now open in: ${[...existingClients, clientId].map(id => this.formatClientId(id)).join(', ')})`);
|
|
780
|
+
}
|
|
498
781
|
refreshContent(uri, clientId) {
|
|
499
782
|
const syncedDocument = this.__syncedDocuments.get(this.documentKey(uri));
|
|
500
783
|
if (syncedDocument) {
|
|
501
784
|
// Trigger a (re-)build by firing a change event.
|
|
502
|
-
const timer = this.startTimerForUri(syncedDocument.uri, `Refresh synced document: Version ${syncedDocument.version} by ${clientId}`);
|
|
785
|
+
const timer = this.startTimerForUri(syncedDocument.uri, `Refresh synced document: Version ${syncedDocument.version} by ${this.formatClientId(clientId)}`);
|
|
503
786
|
this.__onDidChangeContent.fire(Object.freeze({ document: syncedDocument, clientId }));
|
|
504
787
|
timer.dispose();
|
|
505
788
|
}
|
|
@@ -511,7 +794,7 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
511
794
|
* two URIs for one physical file (a symlink path and its real path) into a
|
|
512
795
|
* single registration — dedup at the editor layer, not just in
|
|
513
796
|
* `LangiumDocuments`. The URI the client opened under is preserved separately
|
|
514
|
-
* for egress addressing (see {@link
|
|
797
|
+
* for egress addressing (see {@link DocumentTrackingRecord.languageClientUris}). The
|
|
515
798
|
* policy is always bound (the framework defaults it to
|
|
516
799
|
* `DefaultDocumentUriPolicy`, where canonical ≡ syntactic normalize).
|
|
517
800
|
*/
|
|
@@ -533,7 +816,7 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
533
816
|
trackingFor(uri) {
|
|
534
817
|
let record = this.__documents.get(uri);
|
|
535
818
|
if (!record) {
|
|
536
|
-
record = {
|
|
819
|
+
record = { versionAuthors: [], clientVersions: new Map() };
|
|
537
820
|
this.__documents.set(uri, record);
|
|
538
821
|
}
|
|
539
822
|
return record;
|
|
@@ -566,7 +849,7 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
566
849
|
* `0` for a URI this store has never seen. The shared sequence is
|
|
567
850
|
* server-owned and monotonic across close/reopen cycles, and advances
|
|
568
851
|
* exactly when the synced content changes — which is what makes it a sound
|
|
569
|
-
* optimistic-concurrency token (
|
|
852
|
+
* optimistic-concurrency token (based-on gates): version unchanged ⇔
|
|
570
853
|
* content unchanged.
|
|
571
854
|
*/
|
|
572
855
|
version(uri) {
|
|
@@ -606,6 +889,57 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
606
889
|
this.logUri(key, `External content change while closed: sequence stepped to version ${stepped.version}`, 'debug');
|
|
607
890
|
return stepped.version;
|
|
608
891
|
}
|
|
892
|
+
/**
|
|
893
|
+
* Commit an integrity repair into the OPEN document for `uri`, returning the
|
|
894
|
+
* document that now holds it, or `undefined` when there was nothing here to
|
|
895
|
+
* commit into.
|
|
896
|
+
*
|
|
897
|
+
* The one thing this exists to do is address the store BY URI. A repair
|
|
898
|
+
* reaches the resync as a text-document object, and that object is the
|
|
899
|
+
* store's own only on the LSP path; a document built for an already-open URI
|
|
900
|
+
* through `LangiumDocumentFactory.fromString` carries its own, so writing the
|
|
901
|
+
* repair into it corrects a copy the store does not know about and the editor
|
|
902
|
+
* never sees.
|
|
903
|
+
*
|
|
904
|
+
* `parsedFrom` is the text the repaired AST was parsed from, and a mismatch
|
|
905
|
+
* against the current content REFUSES the commit: the store has moved on,
|
|
906
|
+
* which means an editor change landed after that parse, and committing would
|
|
907
|
+
* overwrite a newer edit with a repair computed against text the user has
|
|
908
|
+
* already replaced. The build that change provokes recomputes the repair.
|
|
909
|
+
*
|
|
910
|
+
* Deliberately NOT compared by version. A separately created document seeds
|
|
911
|
+
* its own numbering, so requiring the two to agree would refuse every commit
|
|
912
|
+
* on the path this exists for. Content is the thing both sides can be held to.
|
|
913
|
+
*
|
|
914
|
+
* A changed text is a new version authored by {@link INTEGRITY_CLIENT_ID}.
|
|
915
|
+
* Kept at the old version, the repair is invisible to every reader keyed on
|
|
916
|
+
* versions: a write based on the unrepaired version passes the based-on gate
|
|
917
|
+
* and replaces the repair, and an echo filter credits the repair to the
|
|
918
|
+
* client whose edit it corrected. No change event is fired: the repair
|
|
919
|
+
* rides the build already under way, and an event would re-enter it.
|
|
920
|
+
*/
|
|
921
|
+
commitRepair(uri, parsedFrom, repaired) {
|
|
922
|
+
const key = this.documentKey(uri);
|
|
923
|
+
const document = this.__syncedDocuments.get(key);
|
|
924
|
+
if (document === undefined) {
|
|
925
|
+
return { status: 'not-open' };
|
|
926
|
+
}
|
|
927
|
+
if (document.getText() !== parsedFrom) {
|
|
928
|
+
this.logUri(key, 'Refuse repair commit: the open document moved on from the text the AST was parsed from', 'debug');
|
|
929
|
+
return { status: 'stale' };
|
|
930
|
+
}
|
|
931
|
+
if (repaired === parsedFrom) {
|
|
932
|
+
return { status: 'committed', document };
|
|
933
|
+
}
|
|
934
|
+
// Reassigned rather than mutated in place: the default configuration
|
|
935
|
+
// updates and returns the SAME instance, but an adopter-supplied one may
|
|
936
|
+
// return a new object, and the store must end up holding whichever it is.
|
|
937
|
+
const updated = this.configuration.update(document, [{ text: repaired }], document.version + 1);
|
|
938
|
+
this.__syncedDocuments.set(key, updated);
|
|
939
|
+
this.setAuthor(key, updated.version, INTEGRITY_CLIENT_ID);
|
|
940
|
+
this.log(updated.uri, `Update to version ${updated.version} by ${this.formatClientId(INTEGRITY_CLIENT_ID)} (repair)`);
|
|
941
|
+
return { status: 'committed', document: updated };
|
|
942
|
+
}
|
|
609
943
|
getAuthor(uri, version) {
|
|
610
944
|
const history = this.__documents.get(this.documentKey(uri))?.versionAuthors;
|
|
611
945
|
// Either the requested version, or the latest. `version !== undefined` so we treat 0 correctly.
|
|
@@ -624,16 +958,16 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
624
958
|
* True iff any client still holds `uri` open. Distinct from {@link isOpen},
|
|
625
959
|
* which reads `__syncedDocuments` — that map is cleared only AFTER the
|
|
626
960
|
* `onDidClose` event fires for the last client, so `isOpen` returns `true`
|
|
627
|
-
* during the close event itself. `isOpenInAnyClient` reads the
|
|
628
|
-
*
|
|
961
|
+
* during the close event itself. `isOpenInAnyClient` reads the open table
|
|
962
|
+
* ({@link __sessions}), which is updated BEFORE the fire, so an `onDidClose`
|
|
629
963
|
* subscriber that finds this `false` knows the last client just closed and a
|
|
630
964
|
* disk re-read / rebuild can proceed.
|
|
631
965
|
*/
|
|
632
966
|
isOpenInAnyClient(uri) {
|
|
633
|
-
return
|
|
967
|
+
return this.__sessions.isOpen(this.documentKey(uri));
|
|
634
968
|
}
|
|
635
969
|
isOpenInClient(uri, client) {
|
|
636
|
-
return
|
|
970
|
+
return this.__sessions.isOpenIn(this.documentKey(uri), client);
|
|
637
971
|
}
|
|
638
972
|
isOpenInLanguageClient(uri) {
|
|
639
973
|
// Takes any URI form (canonicalizes internally via `documentKey`, like
|
|
@@ -642,13 +976,13 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
642
976
|
return this.isOpenInClient(uri, LANGUAGE_CLIENT_ID);
|
|
643
977
|
}
|
|
644
978
|
isOnlyOpenInClient(uri, client) {
|
|
645
|
-
const
|
|
646
|
-
return
|
|
979
|
+
const clients = this.__sessions.clientsOf(this.documentKey(uri));
|
|
980
|
+
return clients.length === 1 && clients[0] === client;
|
|
647
981
|
}
|
|
648
982
|
/**
|
|
649
983
|
* Every document currently held open by at least one client, with the client
|
|
650
|
-
* ids holding it. Reads the same
|
|
651
|
-
*
|
|
984
|
+
* ids holding it. Reads the same open table as {@link isOpenInAnyClient}, so
|
|
985
|
+
* it reflects a last-client close immediately.
|
|
652
986
|
*
|
|
653
987
|
* Diagnostics-oriented — the server-state snapshot lists these so an operator
|
|
654
988
|
* can see WHY a document is pinned: a document that lingers here after its
|
|
@@ -656,27 +990,133 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
656
990
|
* under a shedding policy, explains why its CST is never shed).
|
|
657
991
|
*/
|
|
658
992
|
openDocuments() {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
993
|
+
return this.__sessions.openDocuments();
|
|
994
|
+
}
|
|
995
|
+
/** Fires once a client session has ended, after every document it had open was closed. */
|
|
996
|
+
get onDidCloseSession() {
|
|
997
|
+
return this.__sessions.onDidCloseSession;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Fires once a document no client has open is released: at its last close,
|
|
1001
|
+
* or, for a lost client's last close, when its revert grace runs out,
|
|
1002
|
+
* another client opens it, or it is deleted. The document then reverts to
|
|
1003
|
+
* disk.
|
|
1004
|
+
*/
|
|
1005
|
+
get onDidCloseLastOpen() {
|
|
1006
|
+
return this.lastOpenClosedEmitter.event;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Fires for every save the language client reports of a document it has
|
|
1010
|
+
* open: the editor has written the file. {@link onDidSave} follows only
|
|
1011
|
+
* when the file holds the store's text.
|
|
1012
|
+
*/
|
|
1013
|
+
get onDidSaveInLanguageClient() {
|
|
1014
|
+
return this.languageClientSavedEmitter.event;
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Whether `uri` is waiting out the revert grace: its last open closed with a
|
|
1018
|
+
* lost connection, and it still holds its unsaved text. Such a document is
|
|
1019
|
+
* open for no client, yet not closed either, so a caller that would persist
|
|
1020
|
+
* a closed document's text to disk treats it as open.
|
|
1021
|
+
*/
|
|
1022
|
+
isRevertPending(uri) {
|
|
1023
|
+
return this.__sessions.isRevertPending(this.documentKey(uri));
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Start a client session under `clientId`. Throws `DuplicateClientIdError`
|
|
1027
|
+
* where {@link ClientSessionRegistry.register} refuses the id.
|
|
1028
|
+
*/
|
|
1029
|
+
registerSession(clientId) {
|
|
1030
|
+
this.__sessions.register(clientId);
|
|
1031
|
+
this.tracer.info(`Session started: ${this.formatClientId(clientId)}`);
|
|
1032
|
+
this.tracer.trace(`Session started: ${clientId}`);
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* End the client session `clientId`: close every document it has open, then
|
|
1036
|
+
* free the id. Immediate — each close runs the ordinary close path before
|
|
1037
|
+
* this returns, with `cause` as its cause. A no-op for an id that is not a
|
|
1038
|
+
* registered session.
|
|
1039
|
+
*/
|
|
1040
|
+
closeSession(clientId, cause = 'closed') {
|
|
1041
|
+
if (!this.__sessions.isRegistered(clientId)) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
for (const uri of this.__sessions.beginClose(clientId)) {
|
|
1046
|
+
this.notifyDidCloseTextDocument({ textDocument: { uri } }, clientId, cause);
|
|
663
1047
|
}
|
|
664
1048
|
}
|
|
665
|
-
|
|
1049
|
+
finally {
|
|
1050
|
+
this.__sessions.unregister(clientId);
|
|
1051
|
+
this.tracer.info(`Session closed: ${this.formatClientId(clientId)}`);
|
|
1052
|
+
this.tracer.trace(`Session closed: ${clientId}`);
|
|
1053
|
+
}
|
|
666
1054
|
}
|
|
667
1055
|
/**
|
|
668
|
-
*
|
|
1056
|
+
* The file behind `uri` was deleted: close every open of it except the
|
|
1057
|
+
* language client's.
|
|
1058
|
+
*
|
|
1059
|
+
* The editor's open is left alone because the editor owns it: it keeps the
|
|
1060
|
+
* buffer of a deleted file and goes on sending changes for it, and a store
|
|
1061
|
+
* that had closed the document would drop every one of them until the editor
|
|
1062
|
+
* reopened.
|
|
1063
|
+
*/
|
|
1064
|
+
notifyDocumentDeleted(uri) {
|
|
1065
|
+
const key = this.documentKey(uri);
|
|
1066
|
+
for (const clientId of this.__sessions.clientsOf(key)) {
|
|
1067
|
+
if (clientId !== LANGUAGE_CLIENT_ID) {
|
|
1068
|
+
this.notifyDidCloseTextDocument({ textDocument: { uri: key } }, clientId);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Remove the document for `uri`, closing every open of it first — the
|
|
1074
|
+
* editor's included, since the document it would keep editing is gone.
|
|
1075
|
+
*
|
|
1076
|
+
* The base removes the synced document without consulting the open table,
|
|
1077
|
+
* which leaves every open recorded against a document that no longer exists:
|
|
1078
|
+
* the store then answers that the URI is open, and the next open attaches to
|
|
1079
|
+
* nothing.
|
|
1080
|
+
*/
|
|
1081
|
+
delete(uri) {
|
|
1082
|
+
const key = this.documentKey((typeof uri === 'object' && 'uri' in uri ? uri.uri : uri).toString());
|
|
1083
|
+
for (const clientId of this.__sessions.clientsOf(key)) {
|
|
1084
|
+
this.notifyDidCloseTextDocument({ textDocument: { uri: key } }, clientId);
|
|
1085
|
+
}
|
|
1086
|
+
// A document waiting out the grace has no client left to close, and is
|
|
1087
|
+
// released now as its last close would have released it.
|
|
1088
|
+
if (this.__sessions.isRevertPending(key)) {
|
|
1089
|
+
this.__sessions.cancelRevert(key);
|
|
1090
|
+
this.releaseDocument(key);
|
|
1091
|
+
}
|
|
1092
|
+
super.delete(key);
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* A client id as log lines print it. An id containing `#` is cut eight
|
|
1096
|
+
* characters after its last `#`, which leaves a minted `label#uuid` as the
|
|
1097
|
+
* label and enough of the UUID to tell sessions apart; an id without `#`
|
|
1098
|
+
* prints whole. The full id goes out at trace level wherever a session
|
|
1099
|
+
* starts or ends.
|
|
1100
|
+
*/
|
|
1101
|
+
formatClientId(clientId) {
|
|
1102
|
+
const separator = clientId.lastIndexOf('#');
|
|
1103
|
+
return separator < 0 ? clientId : clientId.slice(0, separator + 9);
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Stages integrity-updated content for a document no client holds.
|
|
669
1107
|
*
|
|
670
1108
|
* When `workspace/applyEdit` targets a closed file, the client opens it from
|
|
671
1109
|
* disk (stale) and sends `didOpen` before applying the edit. This staged
|
|
672
1110
|
* content is consumed by {@link notifyDidOpenTextDocument} to replace the
|
|
673
1111
|
* stale disk text, preventing a brief revert of the integrity update.
|
|
674
1112
|
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
1113
|
+
* Only a FIRST open consumes it, so stage only for a URI no client holds
|
|
1114
|
+
* ({@link isOpenInAnyClient} is `false`). A URI held only through another
|
|
1115
|
+
* head is not closed: an editor attaching to it joins the existing entry and
|
|
1116
|
+
* never reads the stage, and the last close discards the stage with the
|
|
1117
|
+
* tracking record. An entry lingers only if `workspace/applyEdit` fails and
|
|
1118
|
+
* the file is never opened — the memory cost is one serialised string per
|
|
1119
|
+
* URI.
|
|
680
1120
|
*/
|
|
681
1121
|
stagePendingContent(uri, text) {
|
|
682
1122
|
this.trackingFor(this.documentKey(uri)).pendingContent = text;
|
|
@@ -729,10 +1169,11 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
729
1169
|
const targets = recorded && recorded.size > 0 ? recorded : [this.toLanguageClientUri(uri)];
|
|
730
1170
|
let lastResult;
|
|
731
1171
|
for (const targetUri of targets) {
|
|
732
|
-
// Read BEFORE computeEdits, which overwrites
|
|
733
|
-
//
|
|
734
|
-
// echo of
|
|
735
|
-
|
|
1172
|
+
// Read BEFORE computeEdits, which overwrites the baseline and drops
|
|
1173
|
+
// the opened snapshot. This is the text the client holds, and
|
|
1174
|
+
// therefore the only text its echo of this push can be reconstructed
|
|
1175
|
+
// against — whether or not the push below is keyed to it.
|
|
1176
|
+
const before = this.__shadow.clientText(targetUri);
|
|
736
1177
|
const edits = this.__shadow.computeEdits(targetUri, newText);
|
|
737
1178
|
if (edits.length === 0) {
|
|
738
1179
|
continue;
|
|
@@ -747,9 +1188,20 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
747
1188
|
// it is exactly what the caller retries with after a rejection, so
|
|
748
1189
|
// gating it there would refuse the recovery too.
|
|
749
1190
|
const version = isFullReplace(edits) ? UNKNOWN_CLIENT_VERSION : this.languageClientVersion(uri);
|
|
1191
|
+
// A full `ApplyWorkspaceEditParams`, `edit` and all — NOT a bare
|
|
1192
|
+
// `WorkspaceEdit` with a `label` beside it. `applyEdit` takes
|
|
1193
|
+
// `ApplyWorkspaceEditParams | WorkspaceEdit` and discriminates on
|
|
1194
|
+
// `!!value.edit`, so `{ label, documentChanges }` is wrapped as
|
|
1195
|
+
// `{ edit: { label, documentChanges } }` — putting the label inside
|
|
1196
|
+
// the edit, where LSP defines no such field and no client reads it.
|
|
1197
|
+
// The union is also what hides it at compile time: excess-property
|
|
1198
|
+
// checking admits a property present in EITHER member, so an object
|
|
1199
|
+
// matching neither type-checks against the union.
|
|
750
1200
|
const result = await connection.workspace.applyEdit({
|
|
751
1201
|
label: options?.label,
|
|
752
|
-
|
|
1202
|
+
edit: {
|
|
1203
|
+
documentChanges: [TextDocumentEdit.create(OptionalVersionedTextDocumentIdentifier.create(targetUri, version), edits)]
|
|
1204
|
+
}
|
|
753
1205
|
});
|
|
754
1206
|
if (result && result.applied === false) {
|
|
755
1207
|
this.__shadow.invalidate(targetUri);
|
|
@@ -803,12 +1255,17 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
803
1255
|
}
|
|
804
1256
|
/**
|
|
805
1257
|
* Decide what an incoming language-client change actually is, by
|
|
806
|
-
* reconstructing the client's resulting buffer against the
|
|
807
|
-
* in flight for
|
|
1258
|
+
* reconstructing the client's resulting buffer against the text its ranges
|
|
1259
|
+
* address — the pre-push buffer of the OLDEST push still in flight for
|
|
1260
|
+
* `clientFacing`, else whatever that client is believed to hold.
|
|
808
1261
|
*
|
|
809
|
-
* `undefined` when
|
|
810
|
-
*
|
|
811
|
-
*
|
|
1262
|
+
* `undefined` when the client's ranges address the synced text itself —
|
|
1263
|
+
* nothing in flight, and no evidence the client holds anything else. That
|
|
1264
|
+
* is the ordinary path, and the caller then applies the ranges directly.
|
|
1265
|
+
* The two texts part company without a push in flight whenever a client
|
|
1266
|
+
* attaches to a document another client has already written: it opened
|
|
1267
|
+
* from disk, so applying its ranges to the synced text splices lines they
|
|
1268
|
+
* never addressed.
|
|
812
1269
|
*
|
|
813
1270
|
* **Why the oldest, and why reconstruct at all.** The client applies our
|
|
814
1271
|
* pushes in order and echoes each against the buffer it held before that
|
|
@@ -835,22 +1292,41 @@ export class HydraniumTextDocuments extends NormalizedTextDocuments {
|
|
|
835
1292
|
* queue, so undo revisits PAST states while the queue holds IN-FLIGHT ones.
|
|
836
1293
|
*/
|
|
837
1294
|
classifyLanguageClientChange(clientFacing, document, changes) {
|
|
838
|
-
const
|
|
839
|
-
|
|
1295
|
+
const queued = this.__pendingPushes.get(clientFacing);
|
|
1296
|
+
const pending = queued?.length ? queued : undefined;
|
|
1297
|
+
// The text the client's ranges address: the buffer it held before the
|
|
1298
|
+
// oldest push still in flight, else the buffer it is believed to hold.
|
|
1299
|
+
const clientText = pending?.[0].before ?? this.__shadow.clientText(clientFacing);
|
|
1300
|
+
if (pending === undefined && (clientText === undefined || clientText === document.getText())) {
|
|
840
1301
|
return undefined;
|
|
841
1302
|
}
|
|
1303
|
+
if (pending !== undefined && pending[0].before === undefined && changes.some(change => ContentChange.isIncremental(change))) {
|
|
1304
|
+
// A push sent to a client whose buffer was unknown — a first sync, or
|
|
1305
|
+
// the recovery push after a rejection. The shadow now holds the text
|
|
1306
|
+
// that push MOVES the client to, which is the one text the echo's
|
|
1307
|
+
// ranges provably do not address, so the fallback above is a baseline
|
|
1308
|
+
// known to be wrong rather than merely unverified. Drop the queue and
|
|
1309
|
+
// the shadow: the next sync is then a full replace, which lands on
|
|
1310
|
+
// whatever the client holds. A full-text change is exempt because it
|
|
1311
|
+
// reconstructs identically against any baseline.
|
|
1312
|
+
pending.length = 0;
|
|
1313
|
+
this.__shadow.invalidate(clientFacing);
|
|
1314
|
+
return { kind: 'unreconstructable' };
|
|
1315
|
+
}
|
|
842
1316
|
// Through the configured factories, not `TextDocument` directly, so an
|
|
843
1317
|
// adopter's custom text-document type governs how the ranges are applied
|
|
844
1318
|
// here exactly as it does on the synced document.
|
|
845
|
-
const probe = this.create(clientFacing, document.languageId, 0,
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1319
|
+
const probe = this.create(clientFacing, document.languageId, 0, clientText ?? document.getText());
|
|
1320
|
+
const reconstructed = this.update(probe, changes, 0).getText();
|
|
1321
|
+
if (pending !== undefined) {
|
|
1322
|
+
const matchIndex = pending.findIndex(push => push.afterHash === contentHash(reconstructed));
|
|
1323
|
+
if (matchIndex >= 0) {
|
|
1324
|
+
pending.splice(0, matchIndex + 1);
|
|
1325
|
+
return { kind: 'echo' };
|
|
1326
|
+
}
|
|
1327
|
+
pending.length = 0;
|
|
1328
|
+
}
|
|
1329
|
+
return { kind: 'divergent', text: reconstructed };
|
|
854
1330
|
}
|
|
855
1331
|
/**
|
|
856
1332
|
* Explicitly baseline the language-client text shadow for a URI. Useful in
|