@hydranium/core 1.0.0-next.18 → 1.0.0-next.181
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 +190 -30
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +236 -40
- 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 +187 -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 +447 -82
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +780 -160
- 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-extension-service.d.ts.map +1 -1
- package/lib/langium/ast-extension/ast-extension-service.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/build-phase-pass/build-phase-pass-service.d.ts.map +1 -1
- package/lib/langium/build-phase-pass/build-phase-pass-service.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/config/settings.js.map +1 -1
- package/lib/langium/diagnostics/hydranium-langium-profiler.d.ts.map +1 -1
- package/lib/langium/diagnostics/hydranium-langium-profiler.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/diagnostics/server-tracer.d.ts.map +1 -1
- package/lib/langium/diagnostics/server-tracer.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 +368 -21
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +641 -47
- 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/documentation/comment-provider.d.ts.map +1 -1
- package/lib/langium/documentation/comment-provider.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 +58 -13
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +213 -29
- 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/keys/positional-key-provider.d.ts.map +1 -1
- package/lib/langium/keys/positional-key-provider.js.map +1 -1
- package/lib/langium/labeling/label-provider.d.ts.map +1 -1
- package/lib/langium/labeling/label-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 +307 -0
- package/lib/langium/model-service/client-session.d.ts.map +1 -0
- package/lib/langium/model-service/client-session.js +395 -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 +276 -226
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +251 -375
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +144 -35
- package/lib/langium/module.d.ts.map +1 -1
- package/lib/langium/module.js +46 -35
- 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/project/abstract-project-manager.d.ts.map +1 -1
- package/lib/langium/project/abstract-project-manager.js +9 -2
- package/lib/langium/project/abstract-project-manager.js.map +1 -1
- package/lib/langium/project/project-change-event.d.ts +2 -2
- package/lib/langium/project/single-project-manager.d.ts.map +1 -1
- package/lib/langium/project/single-project-manager.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/ast-node-description-provider.d.ts.map +1 -1
- package/lib/langium/scope/ast-node-description-provider.js.map +1 -1
- package/lib/langium/scope/hydranium-scope-computation.d.ts.map +1 -1
- package/lib/langium/scope/hydranium-scope-computation.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/scope/reference-builder.d.ts.map +1 -1
- package/lib/langium/scope/reference-builder.js.map +1 -1
- package/lib/langium/scope/reference-candidate-provider.d.ts.map +1 -1
- package/lib/langium/scope/reference-candidate-provider.js.map +1 -1
- package/lib/langium/scope/scope-extension-service.d.ts.map +1 -1
- package/lib/langium/scope/scope-extension-service.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/serialization/json-serializer.d.ts +1 -1
- package/lib/langium/serialization/json-serializer.d.ts.map +1 -1
- package/lib/langium/serialization/json-serializer.js.map +1 -1
- package/lib/langium/serialization/yaml-serializer.d.ts.map +1 -1
- package/lib/langium/serialization/yaml-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/normalize-empty-strings.d.ts.map +1 -1
- package/lib/langium/update-rewrite/update-rewrite-service.d.ts.map +1 -1
- package/lib/langium/update-rewrite/update-rewrite-service.js.map +1 -1
- 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/validation/validation-contribution-collector.d.ts.map +1 -1
- package/lib/langium/validation/validation-contribution-collector.js.map +1 -1
- package/lib/langium/workspace/document-uri-policy.d.ts +17 -12
- package/lib/langium/workspace/document-uri-policy.d.ts.map +1 -1
- package/lib/langium/workspace/document-uri-policy.js +10 -9
- package/lib/langium/workspace/document-uri-policy.js.map +1 -1
- package/lib/langium/workspace/file-not-found.d.ts +17 -0
- package/lib/langium/workspace/file-not-found.d.ts.map +1 -0
- package/lib/langium/workspace/file-not-found.js +20 -0
- package/lib/langium/workspace/file-not-found.js.map +1 -0
- package/lib/langium/workspace/file-system-provider.d.ts +84 -8
- package/lib/langium/workspace/file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/file-system-provider.js +93 -14
- 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-lock.d.ts +43 -1
- package/lib/langium/workspace/hydranium-workspace-lock.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.js +58 -1
- package/lib/langium/workspace/hydranium-workspace-lock.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts +81 -8
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.js +128 -10
- package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts +25 -2
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.js +41 -15
- package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
- package/lib/langium/workspace/index-manager.d.ts.map +1 -1
- package/lib/langium/workspace/index-manager.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 +91 -29
- package/lib/langium/workspace/langium-documents.js.map +1 -1
- package/lib/langium/workspace/persistent-file-system-provider.d.ts +0 -7
- package/lib/langium/workspace/persistent-file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/persistent-file-system-provider.js +0 -7
- package/lib/langium/workspace/persistent-file-system-provider.js.map +1 -1
- package/lib/langium/workspace/virtual-document.d.ts +48 -15
- package/lib/langium/workspace/virtual-document.d.ts.map +1 -1
- package/lib/langium/workspace/virtual-document.js +78 -18
- package/lib/langium/workspace/virtual-document.js.map +1 -1
- package/lib/langium/workspace/write-lock-scope.d.ts +20 -10
- package/lib/langium/workspace/write-lock-scope.d.ts.map +1 -1
- package/lib/langium/workspace/write-lock-scope.js +12 -4
- package/lib/langium/workspace/write-lock-scope.js.map +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 +90 -79
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +152 -99
- 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/event-loop-monitor.js.map +1 -1
- 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/lint-grammar.js.map +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/memory-monitor.js.map +1 -1
- package/lib/node/node-file-system-provider.d.ts +39 -4
- package/lib/node/node-file-system-provider.d.ts.map +1 -1
- package/lib/node/node-file-system-provider.js +109 -49
- package/lib/node/node-file-system-provider.js.map +1 -1
- package/lib/node/process-memory.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/profile-digest.js.map +1 -1
- package/lib/node/profiling-run.d.ts +2 -2
- package/lib/node/profiling-run.d.ts.map +1 -1
- 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 +16 -1
- package/lib/node/socket-launcher.d.ts.map +1 -1
- package/lib/node/socket-launcher.js +4 -3
- package/lib/node/socket-launcher.js.map +1 -1
- package/lib/node/stdio-launcher.d.ts +1 -1
- package/lib/node/stdio-launcher.d.ts.map +1 -1
- package/lib/node/stdio-launcher.js +1 -1
- package/lib/node/stdio-launcher.js.map +1 -1
- package/lib/testing/document-uri-policy-conformance.js.map +1 -1
- package/lib/testing/fake-description.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/index.d.ts.map +1 -1
- package/lib/testing/index.js +1 -0
- package/lib/testing/index.js.map +1 -1
- package/lib/testing/langium-test-helpers.d.ts.map +1 -1
- package/lib/testing/langium-test-helpers.js +1 -0
- package/lib/testing/langium-test-helpers.js.map +1 -1
- package/lib/testing/make-noop-language-services.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 +62 -14
- package/lib/testing/make-test-services.d.ts.map +1 -1
- package/lib/testing/make-test-services.js +30 -9
- package/lib/testing/make-test-services.js.map +1 -1
- package/lib/testing/make-test-tracer.js.map +1 -1
- package/lib/testing/node/golden-corpus.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/browser-capture-bridge.js.map +1 -1
- package/lib/testing/playwright/e2e-profiling.js.map +1 -1
- 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 +17 -9
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-document-builder.d.ts +26 -11
- package/lib/testing/stub-document-builder.d.ts.map +1 -1
- package/lib/testing/stub-document-builder.js +57 -4
- package/lib/testing/stub-document-builder.js.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.d.ts +22 -7
- package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.js +92 -15
- package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
- package/lib/testing/stub-index-manager.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/testing/stub-project-manager.js.map +1 -1
- package/lib/testing/stub-service-registry.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/lib/util/environment.d.ts.map +1 -1
- package/lib/util/environment.js.map +1 -1
- package/lib/util/registry.d.ts.map +1 -1
- package/package.json +35 -26
- package/src/documents/ast-document-manager.ts +349 -65
- package/src/documents/client-ids.ts +5 -22
- package/src/documents/client-session-errors.ts +24 -0
- package/src/documents/client-session-registry.ts +240 -0
- package/src/documents/file-system-task-queue.ts +68 -0
- package/src/documents/hydranium-text-documents.ts +973 -202
- 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 +724 -46
- 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 +229 -30
- 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 +606 -0
- package/src/langium/model-service/index.ts +1 -0
- package/src/langium/model-service/model-service.ts +418 -399
- package/src/langium/module.ts +193 -64
- package/src/langium/naming/name-provider.ts +27 -5
- package/src/langium/naming/name-separator-validation.ts +34 -5
- package/src/langium/project/abstract-project-manager.ts +9 -1
- package/src/langium/project/project-change-event.ts +2 -2
- 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 +17 -12
- package/src/langium/workspace/file-not-found.ts +23 -0
- package/src/langium/workspace/file-system-provider.ts +153 -16
- package/src/langium/workspace/hydranium-langium-document-factory.ts +34 -0
- package/src/langium/workspace/hydranium-workspace-lock.ts +86 -2
- package/src/langium/workspace/hydranium-workspace-manager.ts +153 -13
- package/src/langium/workspace/in-memory-file-system-provider.ts +47 -16
- package/src/langium/workspace/initialize-workspace.ts +43 -5
- package/src/langium/workspace/langium-documents.ts +113 -33
- package/src/langium/workspace/persistent-file-system-provider.ts +0 -7
- package/src/langium/workspace/virtual-document.ts +96 -19
- package/src/langium/workspace/write-lock-scope.ts +24 -11
- 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 +182 -106
- 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 +122 -51
- 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 +19 -4
- package/src/node/stdio-launcher.ts +1 -2
- package/src/testing/fake-document.ts +24 -5
- package/src/testing/index.ts +1 -0
- package/src/testing/langium-test-helpers.ts +1 -0
- package/src/testing/make-noop-shared-services.ts +50 -2
- package/src/testing/make-test-services.ts +106 -30
- 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 +28 -21
- package/src/testing/stub-document-builder.ts +83 -17
- package/src/testing/stub-hydranium-text-documents.ts +127 -22
- 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/src/util/environment.ts +9 -1
- 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
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// on lsp-server, contradicting the peer architecture.
|
|
21
21
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
22
22
|
import { NormalizedTextDocuments } from '@hydranium/langium/lsp';
|
|
23
|
-
import { UriUtils } from '@hydranium/langium';
|
|
23
|
+
import { type URI, UriUtils } from '@hydranium/langium';
|
|
24
24
|
import { type ServerSharedServices } from '../langium/module.js';
|
|
25
25
|
import {
|
|
26
26
|
type ApplyWorkspaceEditResult,
|
|
@@ -31,12 +31,13 @@ import {
|
|
|
31
31
|
type DidOpenTextDocumentParams,
|
|
32
32
|
type DidSaveTextDocumentParams,
|
|
33
33
|
type Disposable,
|
|
34
|
-
|
|
34
|
+
Emitter,
|
|
35
35
|
type Event,
|
|
36
36
|
type HandlerResult,
|
|
37
37
|
OptionalVersionedTextDocumentIdentifier,
|
|
38
38
|
type RequestHandler,
|
|
39
39
|
type TextDocumentChangeEvent,
|
|
40
|
+
TextDocumentContentChangeEvent as ContentChange,
|
|
40
41
|
TextDocumentEdit,
|
|
41
42
|
TextDocumentSyncKind,
|
|
42
43
|
type TextDocumentWillSaveEvent,
|
|
@@ -45,9 +46,21 @@ import {
|
|
|
45
46
|
type WillSaveTextDocumentParams
|
|
46
47
|
} from 'vscode-languageserver';
|
|
47
48
|
import { type DocumentUri, TextDocument, type TextDocumentContentChangeEvent } from 'vscode-languageserver-textdocument';
|
|
48
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
type CanonicalUri,
|
|
51
|
+
type LanguageClientUri,
|
|
52
|
+
asLanguageClientUri,
|
|
53
|
+
DisposableCollection,
|
|
54
|
+
type Stopwatch,
|
|
55
|
+
textHash,
|
|
56
|
+
type Tracer
|
|
57
|
+
} from '@hydranium/protocol';
|
|
49
58
|
import { type LogNameOptions } from '../langium/diagnostics/logger.js';
|
|
59
|
+
import { HYDRANIUM_BUILD_REASONS } from '../langium/document-builder/document-builder.js';
|
|
60
|
+
import { isConnectionGoneError } from '../util/connection-liveness.js';
|
|
50
61
|
import { LANGUAGE_CLIENT_ID } from './client-ids.js';
|
|
62
|
+
import { INTEGRITY_CLIENT_ID } from '../langium/integrity/integrity-rule.js';
|
|
63
|
+
import { type ClientSessionClosedEvent, ClientSessionRegistry, type SessionEndCause } from './client-session-registry.js';
|
|
51
64
|
import { isFullReplace, LanguageClientTextShadow } from './language-client-text-shadow.js';
|
|
52
65
|
|
|
53
66
|
/**
|
|
@@ -74,6 +87,62 @@ export interface HydraniumTextDocumentsOptions<T extends TextDocument = TextDocu
|
|
|
74
87
|
* Adopters with a custom text-document type (rare) pass their factory.
|
|
75
88
|
*/
|
|
76
89
|
readonly configuration?: TextDocumentsConfiguration<T>;
|
|
90
|
+
/**
|
|
91
|
+
* How long a document whose last open closed because its client's
|
|
92
|
+
* connection was lost keeps its text before it reverts to disk. An open by
|
|
93
|
+
* a client lost from the document, within this time of its own loss,
|
|
94
|
+
* cancels the revert, so a client that registers again under its id after
|
|
95
|
+
* a dropped connection finds its unsaved edits. Any other open releases the
|
|
96
|
+
* document first and then opens it as a first open does, from the text the
|
|
97
|
+
* opener supplies (an editor's own) or else from the file. Meanwhile the
|
|
98
|
+
* document counts as open for the integrity service, which therefore writes
|
|
99
|
+
* none of its unsaved text to disk. A close the client makes itself, or
|
|
100
|
+
* ending its session, reverts at once whatever this is.
|
|
101
|
+
*
|
|
102
|
+
* Defaults to 10 s. `0` reverts such a document at once as well, released
|
|
103
|
+
* in the close itself rather than on a timer.
|
|
104
|
+
*/
|
|
105
|
+
readonly revertGraceMs?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The default of {@link HydraniumTextDocumentsOptions.revertGraceMs}: long
|
|
110
|
+
* enough for a client whose connection dropped to register again and reopen
|
|
111
|
+
* its documents, which a data client does as soon as it has a connection.
|
|
112
|
+
*/
|
|
113
|
+
const DEFAULT_REVERT_GRACE_MS = 10_000;
|
|
114
|
+
|
|
115
|
+
/** Delivered by {@link HydraniumTextDocuments.onDidSaveInLanguageClient}. */
|
|
116
|
+
export interface LanguageClientSavedEvent {
|
|
117
|
+
readonly uri: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Delivered by {@link HydraniumTextDocuments.onDidCloseLastOpen}. */
|
|
121
|
+
export interface LastOpenClosedEvent {
|
|
122
|
+
readonly uri: CanonicalUri;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Delivered by {@link HydraniumTextDocuments.onDidChangeDirty}. */
|
|
126
|
+
export interface DocumentDirtyChangedEvent {
|
|
127
|
+
readonly uri: CanonicalUri;
|
|
128
|
+
/** The new answer of {@link HydraniumTextDocuments.isDirty}. */
|
|
129
|
+
readonly dirty: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The language client's open of a document under one URI, from its didOpen to
|
|
134
|
+
* its didClose. Each URI is its own editor buffer with its own version counter,
|
|
135
|
+
* so a file reached through a symlink and its real path has one of these each.
|
|
136
|
+
*/
|
|
137
|
+
export interface LanguageClientDocumentState {
|
|
138
|
+
/** The version the client last declared for this URI. */
|
|
139
|
+
declaredVersion: number;
|
|
140
|
+
/**
|
|
141
|
+
* The version an applied versioned push moved this URI to, ahead of its
|
|
142
|
+
* echo. Apart from `declaredVersion`, whose staleness guard would drop that
|
|
143
|
+
* echo and strand its pending push.
|
|
144
|
+
*/
|
|
145
|
+
pushedVersion?: number;
|
|
77
146
|
}
|
|
78
147
|
|
|
79
148
|
/**
|
|
@@ -89,32 +158,60 @@ export interface HydraniumTextDocumentsOptions<T extends TextDocument = TextDocu
|
|
|
89
158
|
* separately-tested diff/apply-verify abstraction that owns its own baseline
|
|
90
159
|
* text; folding its storage here would couple a clean utility to this record
|
|
91
160
|
* for no real gain.
|
|
161
|
+
*
|
|
162
|
+
* Returned by the `protected` {@link HydraniumTextDocuments.trackingFor}, so an
|
|
163
|
+
* override has to name it. Restating the shape structurally instead compiles
|
|
164
|
+
* until a field is added here, and then fails at the adopter rather than at the
|
|
165
|
+
* change that caused it.
|
|
92
166
|
*/
|
|
93
|
-
interface
|
|
94
|
-
/** Client ids currently holding this document open (multi-client membership). */
|
|
95
|
-
readonly clients: Set<string>;
|
|
167
|
+
export interface DocumentTrackingRecord {
|
|
96
168
|
/** Author of each version, sparse-indexed by the SHARED (server-assigned) version number. */
|
|
97
169
|
readonly versionAuthors: string[];
|
|
98
170
|
/**
|
|
99
171
|
* Last version id each client declared for this document (didOpen baseline,
|
|
100
|
-
* advanced by every accepted didChange)
|
|
101
|
-
* per LSP (Monaco numbers its own
|
|
102
|
-
*
|
|
103
|
-
*
|
|
172
|
+
* advanced by every accepted didChange), for the per-client staleness guard.
|
|
173
|
+
* Client version ids are CLIENT-owned per LSP (Monaco numbers its own
|
|
174
|
+
* buffer) — they never leak into the shared version sequence, which the
|
|
175
|
+
* server assigns (see {@link HydraniumTextDocuments.__versionSequences}).
|
|
176
|
+
* The language client's entry is the latest any of its URIs declared; the
|
|
177
|
+
* store checks and addresses it per URI, through
|
|
178
|
+
* {@link DocumentTrackingRecord.languageClientDocuments}.
|
|
104
179
|
*/
|
|
105
180
|
readonly clientVersions: Map<string, number>;
|
|
106
181
|
/** Content staged by integrity rules for a closed document. Consumed on next open. */
|
|
107
182
|
pendingContent?: string;
|
|
108
183
|
/**
|
|
109
|
-
*
|
|
110
|
-
* document under. Usually one;
|
|
111
|
-
* opened under
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* record on last close.
|
|
184
|
+
* Each URI the LSP textual language client opened this (canonically-keyed)
|
|
185
|
+
* document under, with that open's state. Usually one; more when the same
|
|
186
|
+
* file is opened under a symlink path and its real path. These are the
|
|
187
|
+
* egress addresses: the document is *keyed* by its canonical identity, but
|
|
188
|
+
* Monaco holds it under the URI it opened. The language client holds the
|
|
189
|
+
* document while any entry remains.
|
|
116
190
|
*/
|
|
117
|
-
|
|
191
|
+
languageClientDocuments?: Map<LanguageClientUri, LanguageClientDocumentState>;
|
|
192
|
+
/**
|
|
193
|
+
* The clients whose close of this document was caused by a lost connection
|
|
194
|
+
* and that have not opened it again, each with a stopwatch started at its
|
|
195
|
+
* loss on the store's `Clock`. While the document waits out the revert grace, only an
|
|
196
|
+
* open by one of them within its own
|
|
197
|
+
* {@link HydraniumTextDocumentsOptions.revertGraceMs} of that loss cancels
|
|
198
|
+
* the revert. Every such client counts, not only the last to close: one
|
|
199
|
+
* connection's sessions all end lost together, and any of them may reopen
|
|
200
|
+
* first. Without the time limit, a client that returns late inherits the
|
|
201
|
+
* unsaved text of a holder lost after it. A stopwatch rather than a `now()`
|
|
202
|
+
* reading, because a wall-clock step would otherwise expire a claim early or
|
|
203
|
+
* revive one past its grace, against a grace timer that the step leaves
|
|
204
|
+
* alone. An entry past its grace counts as any other client's and is pruned
|
|
205
|
+
* at the next open of the document.
|
|
206
|
+
*/
|
|
207
|
+
lostClients?: Map<string, Stopwatch>;
|
|
208
|
+
/**
|
|
209
|
+
* The text the server last knew the file to hold, `undefined` for no file.
|
|
210
|
+
* Set by the first open and moved by {@link HydraniumTextDocuments.updateDiskBaseline}.
|
|
211
|
+
*/
|
|
212
|
+
diskBaseline?: string;
|
|
213
|
+
/** The last answer {@link HydraniumTextDocuments.onDidChangeDirty} announced. */
|
|
214
|
+
dirty?: boolean;
|
|
118
215
|
}
|
|
119
216
|
|
|
120
217
|
/**
|
|
@@ -128,16 +225,33 @@ export interface OpenDocument {
|
|
|
128
225
|
readonly clients: readonly string[];
|
|
129
226
|
}
|
|
130
227
|
|
|
228
|
+
/**
|
|
229
|
+
* What {@link HydraniumTextDocuments.commitRepair} did with an integrity repair.
|
|
230
|
+
*
|
|
231
|
+
* Three outcomes rather than a document-or-nothing, because the two ways of
|
|
232
|
+
* getting nothing call for opposite responses: `not-open` means there is no
|
|
233
|
+
* store document to correct and the caller carries on with the one it holds,
|
|
234
|
+
* while `stale` means there IS one and it has moved past the text the repair
|
|
235
|
+
* was computed against — so carrying on would persist a correction over an edit
|
|
236
|
+
* that superseded it. Collapsing them lets the second read as permission to
|
|
237
|
+
* proceed, which is the failure this shape exists to make unrepresentable.
|
|
238
|
+
*/
|
|
239
|
+
export type RepairCommit<T extends TextDocument> =
|
|
240
|
+
| { readonly status: 'committed'; readonly document: T }
|
|
241
|
+
| { readonly status: 'stale' }
|
|
242
|
+
| { readonly status: 'not-open' };
|
|
243
|
+
|
|
131
244
|
/**
|
|
132
245
|
* Where a URI's shared version sequence left off — written once at last-client
|
|
133
246
|
* close, consulted at the next open so the sequence CONTINUES instead of
|
|
134
247
|
* restarting at whatever version id the reopening client declares. One entry
|
|
135
|
-
* of {@link HydraniumTextDocuments.__versionSequences}
|
|
248
|
+
* of {@link HydraniumTextDocuments.__versionSequences} — a `protected` field, so
|
|
249
|
+
* a subclass reading the map has to name what it holds.
|
|
136
250
|
*/
|
|
137
|
-
interface VersionSequence {
|
|
251
|
+
export interface VersionSequence {
|
|
138
252
|
/** The shared version at last-client close. */
|
|
139
253
|
readonly version: number;
|
|
140
|
-
/** {@link
|
|
254
|
+
/** {@link textHash} of the synced text at last-client close. */
|
|
141
255
|
readonly contentHash: string;
|
|
142
256
|
}
|
|
143
257
|
|
|
@@ -161,76 +275,80 @@ interface ConnectionWithTextDocumentSync {
|
|
|
161
275
|
/**
|
|
162
276
|
* One text pushed to the LSP textual language client by
|
|
163
277
|
* {@link HydraniumTextDocuments.applyEditToLanguageClient} whose echo has not
|
|
164
|
-
* come back yet. One entry of {@link HydraniumTextDocuments.__pendingPushes}
|
|
278
|
+
* come back yet. One entry of {@link HydraniumTextDocuments.__pendingPushes} —
|
|
279
|
+
* a `protected` field, so a subclass reading the queue has to name what it holds.
|
|
165
280
|
*/
|
|
166
|
-
interface
|
|
281
|
+
export interface PendingLanguageClientPush {
|
|
167
282
|
/**
|
|
168
|
-
* The text the client held BEFORE this push
|
|
169
|
-
*
|
|
170
|
-
*
|
|
283
|
+
* The text the client held BEFORE this push, and therefore the text its
|
|
284
|
+
* echo addresses with its ranges. Wider than the baseline the push's edits
|
|
285
|
+
* were diffed against: a full replace is sent with no diff baseline and
|
|
286
|
+
* still lands on a buffer the echo is keyed to.
|
|
171
287
|
*
|
|
172
|
-
* `undefined` when
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* application is position-independent.
|
|
288
|
+
* `undefined` only when that buffer is unknown — the client never declared
|
|
289
|
+
* one, or a rejection invalidated what was tracked. The echo is then
|
|
290
|
+
* reconstructed against the synced text, which is sound only for a
|
|
291
|
+
* position-independent (full-text) change.
|
|
177
292
|
*/
|
|
178
293
|
readonly before: string | undefined;
|
|
179
|
-
/** {@link
|
|
294
|
+
/** {@link textHash} of the text this push moves the client to. */
|
|
180
295
|
readonly afterHash: string;
|
|
181
296
|
}
|
|
182
297
|
|
|
183
298
|
/**
|
|
184
299
|
* What an incoming language-client change turns out to be once reconstructed
|
|
185
|
-
* against the
|
|
186
|
-
* {@link HydraniumTextDocuments.classifyLanguageClientChange}.
|
|
300
|
+
* against the buffer its ranges address — the return of
|
|
301
|
+
* {@link HydraniumTextDocuments.classifyLanguageClientChange}. That method is
|
|
302
|
+
* `protected`, so an override has to name every arm it can return.
|
|
187
303
|
*/
|
|
188
|
-
type LanguageClientChangeOrigin =
|
|
304
|
+
export type LanguageClientChangeOrigin =
|
|
189
305
|
/** The client is reporting a text we pushed it. The synced document is already there. */
|
|
190
306
|
| { readonly kind: 'echo' }
|
|
191
307
|
/**
|
|
192
|
-
* The client's buffer
|
|
193
|
-
* a
|
|
194
|
-
* reconstructed text is what it now holds, and is authoritative.
|
|
308
|
+
* The client's buffer holds a text we did not push it — a keystroke that
|
|
309
|
+
* raced a push, or an edit to a buffer the store has already been written
|
|
310
|
+
* past. The reconstructed text is what it now holds, and is authoritative.
|
|
195
311
|
*/
|
|
196
|
-
| { readonly kind: 'divergent'; readonly text: string }
|
|
312
|
+
| { readonly kind: 'divergent'; readonly text: string }
|
|
313
|
+
/**
|
|
314
|
+
* The change carries ranges and no known text addresses them, so no
|
|
315
|
+
* reconstruction is offered. Adopting one anyway splices the document and
|
|
316
|
+
* stores an edit nobody made; dropping costs at most the one keystroke the
|
|
317
|
+
* client still holds and the next push contradicts.
|
|
318
|
+
*/
|
|
319
|
+
| { readonly kind: 'unreconstructable' };
|
|
197
320
|
|
|
198
321
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* DIFFERENT revisions of the same file is practically impossible; 53 bits of
|
|
203
|
-
* a well-mixed hash over full text + length gives that without pulling in
|
|
204
|
-
* `node:crypto` (this module must stay runnable in browser hosts).
|
|
322
|
+
* Whether `err` reports that the file of `target` does not exist, as a Node
|
|
323
|
+
* file system and the framework's providers do: code `ENOENT`, with `target`'s
|
|
324
|
+
* path. A missing file of another document is a different failure.
|
|
205
325
|
*/
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
h1 = Math.imul(h1 ^ code, 2654435761);
|
|
212
|
-
h2 = Math.imul(h2 ^ code, 1597334677);
|
|
213
|
-
}
|
|
214
|
-
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
|
215
|
-
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
216
|
-
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
|
217
|
-
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
218
|
-
return `${(4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36)}:${text.length}`;
|
|
326
|
+
function isFileNotFound(err: unknown, target: URI): boolean {
|
|
327
|
+
if (typeof err !== 'object' || err === null || !('code' in err) || err.code !== 'ENOENT') {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
return !('path' in err) || err.path === target.fsPath;
|
|
219
331
|
}
|
|
220
332
|
|
|
221
333
|
/**
|
|
222
334
|
* Multi-client text-document tracking on top of Langium's `NormalizedTextDocuments`.
|
|
223
335
|
*
|
|
224
336
|
* Adds the framework features used by the integrity, model-server, and GLSP layers:
|
|
225
|
-
* - Per-document client membership (multiple clients can attach to the same
|
|
337
|
+
* - Per-document client membership (multiple clients can attach to the same
|
|
338
|
+
* URI) and the client-session table, both kept by {@link __sessions}.
|
|
226
339
|
* - A SERVER-OWNED shared version sequence: per-URI, monotonic across
|
|
227
340
|
* close/reopen cycles, advancing exactly when the synced content changes.
|
|
228
341
|
* Client-declared version ids (Monaco's buffer numbering) feed only a
|
|
229
342
|
* per-client staleness guard and never leak into the shared sequence —
|
|
230
343
|
* the two are different things (an editor's edit-operation counter vs the
|
|
231
344
|
* document's content-revision number), and splicing them lets versions drift
|
|
232
|
-
* silently past
|
|
345
|
+
* silently past based-on gate holders.
|
|
233
346
|
* - Version-author history so each edit is attributable to its originating client.
|
|
347
|
+
* - The revert to disk once no client has a document open, for every head
|
|
348
|
+
* ({@link revertToDisk}), deferred by
|
|
349
|
+
* {@link HydraniumTextDocumentsOptions.revertGraceMs} after a lost connection.
|
|
350
|
+
* - A disk baseline per open document, and whether its text differs from it
|
|
351
|
+
* ({@link isDirty}).
|
|
234
352
|
* - Pending-content staging used by the integrity service to thread corrections
|
|
235
353
|
* through `workspace/applyEdit` cycles for currently-closed documents.
|
|
236
354
|
* - `didOpen` notifications arriving over the LSP connection wait on the
|
|
@@ -240,23 +358,28 @@ function contentHash(text: string): string {
|
|
|
240
358
|
*/
|
|
241
359
|
export class HydraniumTextDocuments<T extends TextDocument = TextDocument> extends NormalizedTextDocuments<T> {
|
|
242
360
|
/**
|
|
243
|
-
* Per-URI client-facing tracking ({@link
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
|
|
248
|
-
|
|
361
|
+
* Per-URI client-facing tracking ({@link DocumentTrackingRecord}), keyed by
|
|
362
|
+
* canonical URI. One record per URI, so dropping it clears every axis at
|
|
363
|
+
* once. Which client has the document open is kept apart, in
|
|
364
|
+
* {@link __sessions}.
|
|
365
|
+
*/
|
|
366
|
+
protected __documents = new Map<CanonicalUri, DocumentTrackingRecord>();
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Which client has which document open, and which client ids are registered
|
|
370
|
+
* sessions. Every open-state predicate on this class reads it, so an open
|
|
371
|
+
* recorded anywhere else is invisible to the last-close transition.
|
|
249
372
|
*/
|
|
250
|
-
protected
|
|
373
|
+
protected readonly __sessions = new ClientSessionRegistry();
|
|
251
374
|
|
|
252
375
|
/**
|
|
253
376
|
* Per-URI shared-version continuity across close/reopen cycles
|
|
254
377
|
* ({@link VersionSequence}), written at last-client close and consulted by
|
|
255
|
-
* the next first-client open. DELIBERATELY outside {@link
|
|
378
|
+
* the next first-client open. DELIBERATELY outside {@link DocumentTrackingRecord}:
|
|
256
379
|
* that record is deleted on last close, while the version sequence must
|
|
257
380
|
* survive it — the shared version is a server-owned, monotonic,
|
|
258
381
|
* advances-iff-content-changes counter that never resets while the server
|
|
259
|
-
* lives. That invariant is what makes an optimistic
|
|
382
|
+
* lives. That invariant is what makes an optimistic based-on gate
|
|
260
383
|
* sound: "version unchanged ⇔ content unchanged", with no false conflicts
|
|
261
384
|
* from close/reopen version resets and no false passes from a reopened
|
|
262
385
|
* sequence coincidentally landing on a stale writer's number.
|
|
@@ -269,7 +392,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
269
392
|
/**
|
|
270
393
|
* Texts pushed to the LSP textual language client via
|
|
271
394
|
* {@link applyEditToLanguageClient} whose echoes have not come back yet
|
|
272
|
-
* ({@link
|
|
395
|
+
* ({@link PendingLanguageClientPush}), keyed like the shadow by language-client URI.
|
|
273
396
|
* Outbound pushes and inbound echoes are uncorrelated on the wire; this
|
|
274
397
|
* FIFO is the explicit correlation, and it is what
|
|
275
398
|
* {@link classifyLanguageClientChange} reconstructs against.
|
|
@@ -291,7 +414,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
291
414
|
* a pathological echo that never arrives; the memory cost until then is
|
|
292
415
|
* one pre-push text per in-flight push, for milliseconds.
|
|
293
416
|
*/
|
|
294
|
-
protected readonly __pendingPushes = new Map<LanguageClientUri,
|
|
417
|
+
protected readonly __pendingPushes = new Map<LanguageClientUri, PendingLanguageClientPush[]>();
|
|
295
418
|
|
|
296
419
|
/**
|
|
297
420
|
* Tracked text content per URI for the LSP textual language client (Monaco / VS Code).
|
|
@@ -316,6 +439,11 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
316
439
|
|
|
317
440
|
protected readonly tracer: Tracer;
|
|
318
441
|
protected readonly configuration: TextDocumentsConfiguration<T>;
|
|
442
|
+
/** See {@link HydraniumTextDocumentsOptions.revertGraceMs}. */
|
|
443
|
+
protected readonly revertGraceMs: number;
|
|
444
|
+
protected readonly lastOpenClosedEmitter = new Emitter<LastOpenClosedEvent>();
|
|
445
|
+
protected readonly languageClientSavedEmitter = new Emitter<LanguageClientSavedEvent>();
|
|
446
|
+
protected readonly dirtyChangedEmitter = new Emitter<DocumentDirtyChangedEvent>();
|
|
319
447
|
|
|
320
448
|
constructor(
|
|
321
449
|
protected services: ServerSharedServices,
|
|
@@ -329,6 +457,8 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
329
457
|
(uri, reason) => this.tracer.with(uri).warn(`Diff apply-verify fallback (${reason}) — using full-document replace`),
|
|
330
458
|
this
|
|
331
459
|
);
|
|
460
|
+
this.revertGraceMs = options.revertGraceMs ?? DEFAULT_REVERT_GRACE_MS;
|
|
461
|
+
this.onDidCloseLastOpen(event => void this.revertToDisk(event.uri));
|
|
332
462
|
}
|
|
333
463
|
|
|
334
464
|
// Re-exposed configuration factories — for framework-internal callers
|
|
@@ -403,7 +533,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
403
533
|
const disposables = new DisposableCollection();
|
|
404
534
|
disposables.push(
|
|
405
535
|
connection.onDidOpenTextDocument(async (event: DidOpenTextDocumentParams) => {
|
|
406
|
-
await this.
|
|
536
|
+
await this.initialBuildFinished();
|
|
407
537
|
this.notifyDidOpenTextDocument(event);
|
|
408
538
|
})
|
|
409
539
|
);
|
|
@@ -429,7 +559,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
429
559
|
);
|
|
430
560
|
disposables.push(
|
|
431
561
|
connection.onDidSaveTextDocument((event: DidSaveTextDocumentParams) => {
|
|
432
|
-
this.
|
|
562
|
+
void this.notifyLanguageClientSave(event);
|
|
433
563
|
})
|
|
434
564
|
);
|
|
435
565
|
return disposables;
|
|
@@ -458,28 +588,35 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
458
588
|
// client knowing). Gating on the shared version drops real edits in
|
|
459
589
|
// exactly that lag window. A client with no baseline (never opened —
|
|
460
590
|
// a protocol anomaly) falls back to the shared-version compare, the
|
|
461
|
-
// conservative answer.
|
|
591
|
+
// conservative answer. The language client is checked per URI: each is
|
|
592
|
+
// its own buffer, and one URI's higher id would drop the other's edits.
|
|
462
593
|
const record = this.trackingFor(uri);
|
|
463
|
-
const
|
|
594
|
+
const languageClientDocument =
|
|
595
|
+
clientId === LANGUAGE_CLIENT_ID ? record.languageClientDocuments?.get(this.toLanguageClientUri(td.uri)) : undefined;
|
|
596
|
+
const lastSeen = languageClientDocument?.declaredVersion ?? record.clientVersions.get(clientId) ?? document.version;
|
|
464
597
|
if (lastSeen >= td.version) {
|
|
465
598
|
// Distinguish "already at this version" (common: an echo from the client that triggered
|
|
466
599
|
// the update) from "incoming version older than ours" (stale race).
|
|
467
600
|
const reason =
|
|
468
601
|
lastSeen === td.version ? `already at version ${lastSeen}` : `incoming version ${td.version} older than current ${lastSeen}`;
|
|
469
|
-
this.logUri(uri, `Ignore update by ${clientId}: ${reason}`, 'debug');
|
|
602
|
+
this.logUri(uri, `Ignore update by ${this.formatClientId(clientId)}: ${reason}`, 'debug');
|
|
470
603
|
return;
|
|
471
604
|
}
|
|
472
605
|
record.clientVersions.set(clientId, td.version);
|
|
606
|
+
if (languageClientDocument) {
|
|
607
|
+
languageClientDocument.declaredVersion = td.version;
|
|
608
|
+
}
|
|
473
609
|
|
|
474
|
-
// A language-client change
|
|
475
|
-
//
|
|
476
|
-
//
|
|
610
|
+
// A language-client change is keyed to the buffer that client holds,
|
|
611
|
+
// which is the synced text only while the two agree — an authored
|
|
612
|
+
// write advances the synced text without the client knowing, and a
|
|
613
|
+
// push in flight moves the client without the store knowing.
|
|
477
614
|
// Resolve which it is, and against what, before touching anything.
|
|
478
|
-
const
|
|
615
|
+
const origin =
|
|
479
616
|
clientId === LANGUAGE_CLIENT_ID
|
|
480
617
|
? this.classifyLanguageClientChange(this.toLanguageClientUri(td.uri), document, changes)
|
|
481
618
|
: undefined;
|
|
482
|
-
if (
|
|
619
|
+
if (origin?.kind === 'echo') {
|
|
483
620
|
// The client is reporting a text we pushed it, so the synced
|
|
484
621
|
// document is ALREADY there and the changes must not be applied a
|
|
485
622
|
// second time. Nothing is minted and no rebuild fires. The
|
|
@@ -491,19 +628,23 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
491
628
|
this.logUri(uri, `Skip rebuild: echo of a server-authored push (client version ${td.version})`, 'debug');
|
|
492
629
|
return;
|
|
493
630
|
}
|
|
631
|
+
if (origin?.kind === 'unreconstructable') {
|
|
632
|
+
this.tracer.with(uri).warn(`Drop change: no known client buffer for its ranges (client version ${td.version})`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
494
635
|
|
|
495
636
|
// The SHARED version advances iff the content actually changes — the
|
|
496
|
-
// invariant optimistic
|
|
637
|
+
// invariant optimistic based-on gates rely on. The new text is
|
|
497
638
|
// only known after applying the (possibly incremental) changes, so
|
|
498
639
|
// apply at a tentative +1 and roll the version back on an identical
|
|
499
640
|
// result (an empty-changes update only re-stamps the version).
|
|
500
641
|
//
|
|
501
|
-
// A divergent
|
|
502
|
-
//
|
|
503
|
-
//
|
|
642
|
+
// A divergent change is applied as its RECONSTRUCTED text rather than
|
|
643
|
+
// as its own ranges: those ranges address the client's own buffer, so
|
|
644
|
+
// applying them here would splice the wrong lines.
|
|
504
645
|
const previousText = document.getText();
|
|
505
646
|
const sharedVersion = document.version;
|
|
506
|
-
document = this.configuration.update(document,
|
|
647
|
+
document = this.configuration.update(document, origin === undefined ? changes : [{ text: origin.text }], sharedVersion + 1);
|
|
507
648
|
const changed = document.getText() !== previousText;
|
|
508
649
|
if (!changed) {
|
|
509
650
|
document = this.configuration.update(document, [], sharedVersion);
|
|
@@ -511,6 +652,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
511
652
|
this.__syncedDocuments.set(uri, document);
|
|
512
653
|
if (changed) {
|
|
513
654
|
this.setAuthor(uri, document.version, clientId);
|
|
655
|
+
this.refreshDirty(uri);
|
|
514
656
|
}
|
|
515
657
|
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
516
658
|
// Monaco just told us about its new content; record it so the next outbound
|
|
@@ -527,7 +669,10 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
527
669
|
return;
|
|
528
670
|
}
|
|
529
671
|
}
|
|
530
|
-
this.log(
|
|
672
|
+
this.log(
|
|
673
|
+
document.uri,
|
|
674
|
+
`Update to version ${document.version} by ${this.formatClientId(clientId)}${changed ? '' : ' (content unchanged)'}`
|
|
675
|
+
);
|
|
531
676
|
this.__onDidChangeContent.fire(Object.freeze({ document, clientId }));
|
|
532
677
|
}
|
|
533
678
|
}
|
|
@@ -544,7 +689,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
544
689
|
* A content-identical write still fires the change event (rebuild): the
|
|
545
690
|
* authored write's server-side rebuild is correctness-bearing, it just
|
|
546
691
|
* mints no new version — nothing observable changed, so watchers'
|
|
547
|
-
*
|
|
692
|
+
* based-on versions stay valid.
|
|
548
693
|
*
|
|
549
694
|
* Returns the resulting shared version. Throws when the document is not
|
|
550
695
|
* open — callers (`AstDocumentManager.update`) open first.
|
|
@@ -560,57 +705,210 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
560
705
|
document = this.configuration.update(document, [{ text }], document.version + 1);
|
|
561
706
|
this.__syncedDocuments.set(key, document);
|
|
562
707
|
this.setAuthor(key, document.version, clientId);
|
|
708
|
+
this.refreshDirty(key);
|
|
563
709
|
}
|
|
564
|
-
this.log(
|
|
710
|
+
this.log(
|
|
711
|
+
document.uri,
|
|
712
|
+
`Update to version ${document.version} by ${this.formatClientId(clientId)}${changed ? '' : ' (content unchanged)'}`
|
|
713
|
+
);
|
|
565
714
|
this.__onDidChangeContent.fire(Object.freeze({ document, clientId }));
|
|
566
715
|
return document.version;
|
|
567
716
|
}
|
|
568
717
|
|
|
569
|
-
|
|
718
|
+
/**
|
|
719
|
+
* Close `clientId`'s open of the document. When it was the last open, the
|
|
720
|
+
* document is released and reverts to disk, at once or, for a `'lost'`
|
|
721
|
+
* close, after {@link HydraniumTextDocumentsOptions.revertGraceMs}.
|
|
722
|
+
*/
|
|
723
|
+
public notifyDidCloseTextDocument(
|
|
724
|
+
event: DidCloseTextDocumentParams,
|
|
725
|
+
clientId = LANGUAGE_CLIENT_ID,
|
|
726
|
+
cause: SessionEndCause = 'closed'
|
|
727
|
+
): void {
|
|
570
728
|
const uri = this.documentKey(event.textDocument.uri);
|
|
571
|
-
if (
|
|
729
|
+
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
730
|
+
const clientFacing = this.toLanguageClientUri(event.textDocument.uri);
|
|
731
|
+
const languageClientDocuments = this.__documents.get(uri)?.languageClientDocuments;
|
|
732
|
+
if (languageClientDocuments?.delete(clientFacing) && languageClientDocuments.size > 0) {
|
|
733
|
+
// Another URI still holds the document, so only this one's buffer goes.
|
|
734
|
+
this.__shadow.invalidate(clientFacing);
|
|
735
|
+
this.__pendingPushes.delete(clientFacing);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (!this.__sessions.removeOpen(uri, clientId)) {
|
|
572
740
|
return;
|
|
573
741
|
}
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
742
|
+
const record = this.__documents.get(uri);
|
|
743
|
+
record?.clientVersions.delete(clientId);
|
|
744
|
+
if (record && cause === 'lost') {
|
|
745
|
+
(record.lostClients ??= new Map()).set(clientId, this.services.Clock.stopwatch());
|
|
746
|
+
}
|
|
577
747
|
const syncedDocument = this.__syncedDocuments.get(uri);
|
|
578
748
|
if (syncedDocument !== undefined) {
|
|
579
|
-
this.log(syncedDocument.uri, `Closed synced document: ${syncedDocument.version} by ${clientId}`);
|
|
749
|
+
this.log(syncedDocument.uri, `Closed synced document: ${syncedDocument.version} by ${this.formatClientId(clientId)}`);
|
|
580
750
|
this.__onDidClose.fire(Object.freeze({ document: syncedDocument, clientId }));
|
|
581
751
|
|
|
582
752
|
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
583
|
-
// Monaco closed the document; drop the
|
|
584
|
-
//
|
|
585
|
-
//
|
|
753
|
+
// Monaco closed the document; drop the shadow baselined under the URI
|
|
754
|
+
// it held. (If this was the last client the whole record is deleted on
|
|
755
|
+
// release.)
|
|
586
756
|
const droppedUri = this.toLanguageClientUri(event.textDocument.uri);
|
|
587
|
-
this.__documents.get(uri)?.languageClientUris?.delete(droppedUri);
|
|
588
757
|
this.__shadow.invalidate(droppedUri);
|
|
589
758
|
this.__pendingPushes.delete(droppedUri);
|
|
590
759
|
}
|
|
591
|
-
if (!this.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
760
|
+
if (!this.__sessions.isOpen(uri)) {
|
|
761
|
+
if (cause === 'lost' && this.revertGraceMs > 0) {
|
|
762
|
+
this.deferRelease(uri);
|
|
763
|
+
} else {
|
|
764
|
+
this.releaseDocument(uri);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Keep the document, text and all, for the revert grace, then release it.
|
|
772
|
+
* The document stays in the store meanwhile, so a lost client that opens it
|
|
773
|
+
* again within its own grace attaches to it and finds its unsaved text
|
|
774
|
+
* rather than reading disk; see {@link resolvePendingRevert} for any other
|
|
775
|
+
* open.
|
|
776
|
+
*/
|
|
777
|
+
protected deferRelease(uri: CanonicalUri): void {
|
|
778
|
+
this.log(uri, `No client left; revert deferred for ${this.revertGraceMs} ms (connection lost)`);
|
|
779
|
+
const timer = this.services.Clock.setTimer(() => {
|
|
780
|
+
this.__sessions.cancelRevert(uri);
|
|
781
|
+
if (!this.__sessions.isOpen(uri)) {
|
|
782
|
+
this.releaseDocument(uri);
|
|
783
|
+
}
|
|
784
|
+
}, this.revertGraceMs);
|
|
785
|
+
this.__sessions.deferRevert(uri, timer);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Resolve the pending revert of `uri` for an open by `clientId`. An open by a
|
|
790
|
+
* client lost from the document within its grace cancels the revert, and
|
|
791
|
+
* the client finds its unsaved text. Any other open releases the document
|
|
792
|
+
* first, so it opens as a first open does: cancelling for every open hands
|
|
793
|
+
* a lost client's unsaved text to whoever opens next, a reloaded page or an
|
|
794
|
+
* editor, with nothing marking it unsaved.
|
|
795
|
+
*/
|
|
796
|
+
protected resolvePendingRevert(uri: CanonicalUri, clientId: string): void {
|
|
797
|
+
const record = this.__documents.get(uri);
|
|
798
|
+
if (record) {
|
|
799
|
+
this.pruneLostClients(record);
|
|
800
|
+
}
|
|
801
|
+
const returning = record?.lostClients?.delete(clientId) ?? false;
|
|
802
|
+
if (!this.__sessions.isRevertPending(uri)) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
this.__sessions.cancelRevert(uri);
|
|
806
|
+
if (!returning) {
|
|
807
|
+
this.releaseDocument(uri);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** Drop the entries of {@link DocumentTrackingRecord.lostClients} whose grace has run out. */
|
|
812
|
+
protected pruneLostClients(record: DocumentTrackingRecord): void {
|
|
813
|
+
for (const [clientId, sinceLoss] of record.lostClients ?? []) {
|
|
814
|
+
if (sinceLoss.elapsedMs >= this.revertGraceMs) {
|
|
815
|
+
record.lostClients?.delete(clientId);
|
|
610
816
|
}
|
|
611
817
|
}
|
|
612
818
|
}
|
|
613
819
|
|
|
820
|
+
/**
|
|
821
|
+
* Drop the document no client has open any more, then announce it on
|
|
822
|
+
* {@link onDidCloseLastOpen}, which is what reverts it to disk.
|
|
823
|
+
*/
|
|
824
|
+
protected releaseDocument(uri: CanonicalUri): void {
|
|
825
|
+
const syncedDocument = this.__syncedDocuments.get(uri);
|
|
826
|
+
if (syncedDocument === undefined) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
this.log(syncedDocument.uri, `Remove synced document: ${syncedDocument.version} (no client left)`);
|
|
830
|
+
// Persist where the shared version sequence left off (version +
|
|
831
|
+
// content hash) so the next open CONTINUES the sequence instead of
|
|
832
|
+
// restarting at the reopening client's declared id. Hashed once
|
|
833
|
+
// here at release, not on every change.
|
|
834
|
+
this.__versionSequences.set(uri, {
|
|
835
|
+
version: syncedDocument.version,
|
|
836
|
+
contentHash: textHash(syncedDocument.getText())
|
|
837
|
+
});
|
|
838
|
+
const wasDirty = this.__documents.get(uri)?.dirty === true;
|
|
839
|
+
this.__syncedDocuments.delete(uri);
|
|
840
|
+
// One delete clears every per-URI axis (version history + any staged
|
|
841
|
+
// pending content) so a future open with the same URI starts fresh.
|
|
842
|
+
this.__documents.delete(uri);
|
|
843
|
+
if (wasDirty) {
|
|
844
|
+
this.dirtyChangedEmitter.fire(Object.freeze({ uri, dirty: false }));
|
|
845
|
+
}
|
|
846
|
+
this.lastOpenClosedEmitter.fire(Object.freeze({ uri }));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Rebuild a released document from the file system provider, so the build
|
|
851
|
+
* stops carrying the unsaved text of its last client.
|
|
852
|
+
*
|
|
853
|
+
* The provider decides, for every scheme, by `exists`: a document it can
|
|
854
|
+
* serve is rebuilt from its text, and any other — an editor's `untitled:`
|
|
855
|
+
* buffer, a file never saved, or one deleted meanwhile — is removed
|
|
856
|
+
* from the workspace. A `virtual:` document survives, since the framework's
|
|
857
|
+
* provider for that scheme serves it from the index, whatever provider the
|
|
858
|
+
* host passes as `context.fileSystemProvider`; an edited one therefore keeps
|
|
859
|
+
* its last client's text, and keeping it read-only is the client's job.
|
|
860
|
+
*
|
|
861
|
+
* The answer is read in the document's disk queue, so the rebuild follows
|
|
862
|
+
* any save still queued rather than reverting past it; a file that goes
|
|
863
|
+
* after that read is removed when its rebuild finds none.
|
|
864
|
+
*
|
|
865
|
+
* Whether to revert at all is decided inside the write lock, as its holder:
|
|
866
|
+
* decided before waiting for the lock, a client that opens or re-creates the
|
|
867
|
+
* document meanwhile would have its text rebuilt over, or the document
|
|
868
|
+
* removed. A document some client has open again, or that waits out a new
|
|
869
|
+
* grace, is left to that client.
|
|
870
|
+
*/
|
|
871
|
+
protected async revertToDisk(uri: CanonicalUri): Promise<void> {
|
|
872
|
+
const workspace = this.services.workspace;
|
|
873
|
+
const target = UriUtils.toUri(uri);
|
|
874
|
+
const reopened = (): boolean => this.isOpenInAnyClient(uri) || this.__syncedDocuments.has(uri);
|
|
875
|
+
try {
|
|
876
|
+
// Read before the lock: every build and read waits while the lock is
|
|
877
|
+
// held, and this read waits on the file's save I/O.
|
|
878
|
+
const onDisk = await workspace.FileSystemTaskQueue.enqueue(uri, () => workspace.FileSystemProvider.exists(target));
|
|
879
|
+
await workspace.WorkspaceManager?.ready;
|
|
880
|
+
await workspace.WorkspaceLock.write(async token => {
|
|
881
|
+
if (reopened()) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
workspace.DocumentBuilder.markNextReason(HYDRANIUM_BUILD_REASONS.didClose);
|
|
885
|
+
try {
|
|
886
|
+
await (onDisk
|
|
887
|
+
? workspace.DocumentBuilder.update([target], [], token)
|
|
888
|
+
: workspace.DocumentBuilder.update([], [target], token));
|
|
889
|
+
} catch (err: unknown) {
|
|
890
|
+
if (!onDisk || !isFileNotFound(err, target)) {
|
|
891
|
+
throw err;
|
|
892
|
+
}
|
|
893
|
+
// The file went after its existence was read.
|
|
894
|
+
this.tracer.with(uri).debug('Revert found no file; removing the document instead');
|
|
895
|
+
workspace.DocumentBuilder.markNextReason(HYDRANIUM_BUILD_REASONS.didClose);
|
|
896
|
+
await workspace.DocumentBuilder.update([], [target], token);
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
} catch (err: unknown) {
|
|
900
|
+
// A revert that finishes after the LSP peer went away fails its
|
|
901
|
+
// diagnostics publish, and one that runs after its workspace was torn
|
|
902
|
+
// down finds no file: teardown races, not failed reverts.
|
|
903
|
+
if (isConnectionGoneError(err) || isFileNotFound(err, target)) {
|
|
904
|
+
this.tracer.with(uri).debug(`Revert on last close skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
908
|
+
this.tracer.with(uri).error(`Revert on last close dropped. ${detail}`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
614
912
|
public notifyWillSaveTextDocument(event: WillSaveTextDocumentParams): void {
|
|
615
913
|
const syncedDocument = this.__syncedDocuments.get(this.documentKey(event.textDocument.uri));
|
|
616
914
|
if (syncedDocument !== undefined) {
|
|
@@ -630,47 +928,111 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
630
928
|
}
|
|
631
929
|
}
|
|
632
930
|
|
|
931
|
+
/**
|
|
932
|
+
* Handle a save the language client reports: fire
|
|
933
|
+
* {@link onDidSaveInLanguageClient}, then announce it on {@link onDidSave}
|
|
934
|
+
* only once the file read back from disk holds the store's current text.
|
|
935
|
+
*/
|
|
936
|
+
protected async notifyLanguageClientSave(event: DidSaveTextDocumentParams): Promise<void> {
|
|
937
|
+
const syncedDocument = this.__syncedDocuments.get(this.documentKey(event.textDocument.uri));
|
|
938
|
+
if (syncedDocument === undefined) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const uri = syncedDocument.uri;
|
|
942
|
+
this.languageClientSavedEmitter.fire(Object.freeze({ uri }));
|
|
943
|
+
// `onDidSave` tells its listeners that the shared document is on disk.
|
|
944
|
+
// The editor wrote its own buffer, which lags the store while another
|
|
945
|
+
// client's edit is still on its way to it, so only the file says
|
|
946
|
+
// whether that holds. Read it rather than trust the editor: `didSave`
|
|
947
|
+
// carries text only for a client that honours `includeText`, and asking
|
|
948
|
+
// for it means rewriting Langium's initialize answer, which advertises
|
|
949
|
+
// `save` as a Boolean. The read goes through the disk queue and waits on
|
|
950
|
+
// nothing but the provider, so it follows a server save queued
|
|
951
|
+
// meanwhile. A file changed again between the editor's write and this
|
|
952
|
+
// read compares unequal, and the save goes unannounced.
|
|
953
|
+
let onDisk: string | undefined;
|
|
954
|
+
try {
|
|
955
|
+
onDisk = await this.services.workspace.FileSystemTaskQueue.enqueue(uri, () =>
|
|
956
|
+
this.services.workspace.FileSystemProvider.readFile(UriUtils.toUri(uri))
|
|
957
|
+
);
|
|
958
|
+
} catch (err: unknown) {
|
|
959
|
+
this.tracer
|
|
960
|
+
.with(uri)
|
|
961
|
+
.debug(`Editor save not announced: reading the file back failed. ${err instanceof Error ? err.message : String(err)}`);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
// What the file holds, not what the editor meant to write: an editor
|
|
965
|
+
// that saved older text leaves the document dirty.
|
|
966
|
+
this.updateDiskBaseline(uri, onDisk);
|
|
967
|
+
// An editor that saves and then closes releases the document while the
|
|
968
|
+
// read is under way; its save is still a save of the text it held.
|
|
969
|
+
const document = this.__syncedDocuments.get(this.documentKey(uri)) ?? syncedDocument;
|
|
970
|
+
if (onDisk !== document.getText()) {
|
|
971
|
+
this.tracer.with(uri).debug(`Editor save not announced: the file does not hold the text of version ${document.version}`);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
this.announceSave(document, LANGUAGE_CLIENT_ID);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Announce a save the server made. `event.text`, when given, is what the
|
|
979
|
+
* file now holds, written or found there, and becomes the disk baseline
|
|
980
|
+
* before the save is announced. It lags the store when an edit landed
|
|
981
|
+
* after the save took its text, and the document then stays dirty.
|
|
982
|
+
*/
|
|
633
983
|
public notifyDidSaveTextDocument(event: DidSaveTextDocumentParams, clientId = LANGUAGE_CLIENT_ID): void {
|
|
634
984
|
const syncedDocument = this.__syncedDocuments.get(this.documentKey(event.textDocument.uri));
|
|
635
985
|
if (syncedDocument !== undefined) {
|
|
636
|
-
|
|
637
|
-
|
|
986
|
+
if (event.text !== undefined) {
|
|
987
|
+
this.updateDiskBaseline(syncedDocument.uri, event.text);
|
|
988
|
+
}
|
|
989
|
+
this.announceSave(syncedDocument, clientId);
|
|
638
990
|
}
|
|
639
991
|
}
|
|
640
992
|
|
|
993
|
+
/** Fire {@link onDidSave} for `document`, which need not be synced any more. */
|
|
994
|
+
protected announceSave(document: T, clientId: string): void {
|
|
995
|
+
this.log(document.uri, `Saved synced document: ${document.version} by ${this.formatClientId(clientId)}`);
|
|
996
|
+
this.__onDidSave.fire(Object.freeze({ document, clientId }));
|
|
997
|
+
}
|
|
998
|
+
|
|
641
999
|
public notifyDidOpenTextDocument(event: DidOpenTextDocumentParams, clientId = LANGUAGE_CLIENT_ID): void {
|
|
642
1000
|
const td = event.textDocument;
|
|
643
1001
|
const uri = this.documentKey(td.uri);
|
|
1002
|
+
this.resolvePendingRevert(uri, clientId);
|
|
644
1003
|
if (this.isOpenInClient(uri, clientId)) {
|
|
645
1004
|
// Already open for this client under this canonical identity. If this is a
|
|
646
1005
|
// NEW client-facing URI for the same file (a second tab reached via a
|
|
647
|
-
// divergent path, e.g. a symlink and its real path), record it
|
|
648
|
-
//
|
|
649
|
-
//
|
|
1006
|
+
// divergent path, e.g. a symlink and its real path), record it so outbound
|
|
1007
|
+
// edits reach this tab too — but do NOT re-fire open/rebuild; the document
|
|
1008
|
+
// is already live.
|
|
650
1009
|
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
651
1010
|
const clientFacing = this.toLanguageClientUri(td.uri);
|
|
652
1011
|
const record = this.__documents.get(uri);
|
|
653
|
-
if (record && !record.
|
|
654
|
-
(record.
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1012
|
+
if (record && !record.languageClientDocuments?.has(clientFacing)) {
|
|
1013
|
+
(record.languageClientDocuments ??= new Map()).set(clientFacing, { declaredVersion: td.version });
|
|
1014
|
+
// This tab's own buffer, NOT the synced text: a second tab is a second
|
|
1015
|
+
// client model, read from disk, so a server-authored write already
|
|
1016
|
+
// applied to the first tab leaves it BEHIND the synced document. Keying
|
|
1017
|
+
// a diff to the synced text here addresses lines this tab does not have
|
|
1018
|
+
// and drops content it does.
|
|
1019
|
+
this.__shadow.setOpenedText(clientFacing, td.text);
|
|
659
1020
|
}
|
|
660
1021
|
}
|
|
661
1022
|
return;
|
|
662
1023
|
}
|
|
663
1024
|
let document = this.__syncedDocuments.get(uri);
|
|
1025
|
+
const existingClients = this.__sessions.clientsOf(uri);
|
|
1026
|
+
this.__sessions.addOpen(uri, clientId);
|
|
664
1027
|
const record = this.trackingFor(uri);
|
|
665
|
-
const existingClients = [...record.clients];
|
|
666
|
-
record.clients.add(clientId);
|
|
667
1028
|
// Baseline the per-client staleness guard at the version id the client
|
|
668
1029
|
// declared for its own buffer (client-owned per LSP).
|
|
669
1030
|
record.clientVersions.set(clientId, td.version);
|
|
670
1031
|
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
671
1032
|
// Remember the URI Monaco opened under (may differ from the canonical key)
|
|
672
1033
|
// so outbound applyEditToLanguageClient can address the URI it actually holds.
|
|
673
|
-
|
|
1034
|
+
// A fresh state, since a reopened buffer numbers its versions afresh.
|
|
1035
|
+
(record.languageClientDocuments ??= new Map()).set(this.toLanguageClientUri(td.uri), { declaredVersion: td.version });
|
|
674
1036
|
}
|
|
675
1037
|
if (!document) {
|
|
676
1038
|
// Use integrity-staged content if available, otherwise the client-provided (disk) text.
|
|
@@ -679,44 +1041,110 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
679
1041
|
const source = pendingText ? ', source=pending' : '';
|
|
680
1042
|
// The SHARED version is server-assigned: continue the persisted
|
|
681
1043
|
// sequence — same version when the content is unchanged since the
|
|
682
|
-
// last close (so watchers'
|
|
1044
|
+
// last close (so watchers' based-on versions stay valid), one
|
|
683
1045
|
// step when it changed (so no stale pointer can coincidentally pass
|
|
684
1046
|
// the optimistic gate). Only a first-ever open adopts the client's
|
|
685
1047
|
// declared id as the sequence seed.
|
|
686
1048
|
const sequence = this.__versionSequences.get(uri);
|
|
687
1049
|
const version =
|
|
688
|
-
sequence === undefined ? td.version : sequence.contentHash ===
|
|
689
|
-
this.log(uri, `Open document: Version ${version} by ${clientId} [first client${source}]`);
|
|
1050
|
+
sequence === undefined ? td.version : sequence.contentHash === textHash(text) ? sequence.version : sequence.version + 1;
|
|
1051
|
+
this.log(uri, `Open document: Version ${version} by ${this.formatClientId(clientId)} [first client${source}]`);
|
|
690
1052
|
document = this.configuration.create(uri, td.languageId, version, text);
|
|
691
1053
|
this.__syncedDocuments.set(uri, document);
|
|
692
1054
|
this.setAuthor(uri, version, clientId);
|
|
1055
|
+
// The opener's text, not the staged content: a session's open read
|
|
1056
|
+
// it from the file, and an editor opened its buffer from there. An
|
|
1057
|
+
// editor that opens a buffer it never saved is taken as clean.
|
|
1058
|
+
record.diskBaseline = td.text;
|
|
1059
|
+
this.refreshDirty(uri);
|
|
693
1060
|
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
694
1061
|
// Baseline the shadow to what Monaco just opened so the next outbound
|
|
695
1062
|
// applyEditToLanguageClient diffs against the right starting point. Keyed by the
|
|
696
1063
|
// language-client URI (what Monaco holds), not the canonical document key.
|
|
697
|
-
|
|
1064
|
+
//
|
|
1065
|
+
// The CLIENT's declared text, never the synced document: staged content
|
|
1066
|
+
// consumed above leaves the two different, and a baseline asserting the
|
|
1067
|
+
// client already holds the staged text suppresses the one sync that would
|
|
1068
|
+
// deliver it. Skipped entirely when a push is already outstanding for this
|
|
1069
|
+
// URI — an `applyEdit` to a closed file has the client open from disk and
|
|
1070
|
+
// apply afterwards, so that shadow records what it is about to hold and
|
|
1071
|
+
// disk text would key the next diff to a buffer nobody has.
|
|
1072
|
+
const clientFacing = this.toLanguageClientUri(td.uri);
|
|
1073
|
+
if (!this.__shadow.isTracked(clientFacing)) {
|
|
1074
|
+
this.__shadow.set(clientFacing, td.text);
|
|
1075
|
+
}
|
|
698
1076
|
}
|
|
699
1077
|
const toFire = Object.freeze({ document, clientId });
|
|
700
1078
|
this.__onDidOpen.fire(toFire);
|
|
701
1079
|
this.__onDidChangeContent.fire(toFire);
|
|
702
1080
|
} else {
|
|
703
1081
|
// An additional client attaches to a document already open by another client.
|
|
704
|
-
this.
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1082
|
+
this.logClientJoined(uri, clientId, document.version, existingClients);
|
|
1083
|
+
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
1084
|
+
// Monaco's own buffer, which on this path is NOT the synced text — another
|
|
1085
|
+
// client opened the document and may already have changed it. Recorded as
|
|
1086
|
+
// an equality-only baseline (never a diff one, see `setOpenedText`) so the
|
|
1087
|
+
// refresh below does not push a full replace of content Monaco already
|
|
1088
|
+
// holds, which would dirty the file on open.
|
|
1089
|
+
this.__shadow.setOpenedText(this.toLanguageClientUri(td.uri), td.text);
|
|
1090
|
+
}
|
|
709
1091
|
this.refreshContent(uri, clientId);
|
|
710
1092
|
}
|
|
711
1093
|
}
|
|
712
1094
|
|
|
1095
|
+
/**
|
|
1096
|
+
* Record `clientId` as an additional holder of a document another client
|
|
1097
|
+
* already has open.
|
|
1098
|
+
*
|
|
1099
|
+
* Unlike the attach branch of {@link notifyDidOpenTextDocument} this does
|
|
1100
|
+
* not refresh. That branch re-renders an attaching TEXTUAL view; a client
|
|
1101
|
+
* reaching a document through a non-textual route has nothing to re-render,
|
|
1102
|
+
* and refreshing per attach turns each into a Langium rebuild and dependent
|
|
1103
|
+
* relink cascade.
|
|
1104
|
+
*
|
|
1105
|
+
* The staleness guard baselines at the SYNCED version, not a declared buffer
|
|
1106
|
+
* version, because a client arriving this way holds no buffer of its own.
|
|
1107
|
+
* Unifying the two routes therefore rebaselines a textual client's guard to a
|
|
1108
|
+
* version it never declared.
|
|
1109
|
+
*
|
|
1110
|
+
* Returns whether a hold was added — `false` when `uri` is not open,
|
|
1111
|
+
* `clientId` already holds it, or the document was waiting out the revert
|
|
1112
|
+
* grace for other clients and was released instead (see
|
|
1113
|
+
* {@link resolvePendingRevert}); the caller then opens it anew.
|
|
1114
|
+
*/
|
|
1115
|
+
attachClient(uri: DocumentUri, clientId: string): boolean {
|
|
1116
|
+
const key = this.documentKey(uri);
|
|
1117
|
+
if (!this.__syncedDocuments.has(key) || this.isOpenInClient(key, clientId)) {
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
this.resolvePendingRevert(key, clientId);
|
|
1121
|
+
const document = this.__syncedDocuments.get(key);
|
|
1122
|
+
if (!document) {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
const existingClients = this.__sessions.clientsOf(key);
|
|
1126
|
+
this.__sessions.addOpen(key, clientId);
|
|
1127
|
+
const record = this.trackingFor(key);
|
|
1128
|
+
record.clientVersions.set(clientId, document.version);
|
|
1129
|
+
this.logClientJoined(key, clientId, document.version, existingClients);
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
protected logClientJoined(uri: DocumentUri, clientId: string, version: number, existingClients: readonly string[]): void {
|
|
1134
|
+
this.log(
|
|
1135
|
+
uri,
|
|
1136
|
+
`Attach client: ${this.formatClientId(clientId)} joined existing document (version ${version}, ` +
|
|
1137
|
+
`now open in: ${[...existingClients, clientId].map(id => this.formatClientId(id)).join(', ')})`
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
713
1141
|
refreshContent(uri: DocumentUri, clientId: string): void {
|
|
714
1142
|
const syncedDocument = this.__syncedDocuments.get(this.documentKey(uri));
|
|
715
1143
|
if (syncedDocument) {
|
|
716
1144
|
// Trigger a (re-)build by firing a change event.
|
|
717
1145
|
const timer = this.startTimerForUri(
|
|
718
1146
|
syncedDocument.uri,
|
|
719
|
-
`Refresh synced document: Version ${syncedDocument.version} by ${clientId}`
|
|
1147
|
+
`Refresh synced document: Version ${syncedDocument.version} by ${this.formatClientId(clientId)}`
|
|
720
1148
|
);
|
|
721
1149
|
this.__onDidChangeContent.fire(Object.freeze({ document: syncedDocument, clientId }));
|
|
722
1150
|
timer.dispose();
|
|
@@ -730,7 +1158,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
730
1158
|
* two URIs for one physical file (a symlink path and its real path) into a
|
|
731
1159
|
* single registration — dedup at the editor layer, not just in
|
|
732
1160
|
* `LangiumDocuments`. The URI the client opened under is preserved separately
|
|
733
|
-
* for egress addressing (see {@link
|
|
1161
|
+
* for egress addressing (see {@link DocumentTrackingRecord.languageClientDocuments}). The
|
|
734
1162
|
* policy is always bound (the framework defaults it to
|
|
735
1163
|
* `DefaultDocumentUriPolicy`, where canonical ≡ syntactic normalize).
|
|
736
1164
|
*/
|
|
@@ -751,10 +1179,10 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
751
1179
|
}
|
|
752
1180
|
|
|
753
1181
|
/** Get-or-create the per-URI tracking record. `uri` must already be a {@link documentKey}. */
|
|
754
|
-
protected trackingFor(uri: CanonicalUri):
|
|
1182
|
+
protected trackingFor(uri: CanonicalUri): DocumentTrackingRecord {
|
|
755
1183
|
let record = this.__documents.get(uri);
|
|
756
1184
|
if (!record) {
|
|
757
|
-
record = {
|
|
1185
|
+
record = { versionAuthors: [], clientVersions: new Map() };
|
|
758
1186
|
this.__documents.set(uri, record);
|
|
759
1187
|
}
|
|
760
1188
|
return record;
|
|
@@ -790,7 +1218,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
790
1218
|
* `0` for a URI this store has never seen. The shared sequence is
|
|
791
1219
|
* server-owned and monotonic across close/reopen cycles, and advances
|
|
792
1220
|
* exactly when the synced content changes — which is what makes it a sound
|
|
793
|
-
* optimistic-concurrency token (
|
|
1221
|
+
* optimistic-concurrency token (based-on gates): version unchanged ⇔
|
|
794
1222
|
* content unchanged.
|
|
795
1223
|
*/
|
|
796
1224
|
version(uri: DocumentUri): number {
|
|
@@ -822,7 +1250,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
822
1250
|
if (sequence === undefined) {
|
|
823
1251
|
return undefined;
|
|
824
1252
|
}
|
|
825
|
-
const hash =
|
|
1253
|
+
const hash = textHash(text);
|
|
826
1254
|
if (hash === sequence.contentHash) {
|
|
827
1255
|
return sequence.version;
|
|
828
1256
|
}
|
|
@@ -832,6 +1260,59 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
832
1260
|
return stepped.version;
|
|
833
1261
|
}
|
|
834
1262
|
|
|
1263
|
+
/**
|
|
1264
|
+
* Commit an integrity repair into the OPEN document for `uri`, returning the
|
|
1265
|
+
* document that now holds it, or `undefined` when there was nothing here to
|
|
1266
|
+
* commit into.
|
|
1267
|
+
*
|
|
1268
|
+
* The one thing this exists to do is address the store BY URI. A repair
|
|
1269
|
+
* reaches the resync as a text-document object, and that object is the
|
|
1270
|
+
* store's own only on the LSP path; a document built for an already-open URI
|
|
1271
|
+
* through `LangiumDocumentFactory.fromString` carries its own, so writing the
|
|
1272
|
+
* repair into it corrects a copy the store does not know about and the editor
|
|
1273
|
+
* never sees.
|
|
1274
|
+
*
|
|
1275
|
+
* `parsedFrom` is the text the repaired AST was parsed from, and a mismatch
|
|
1276
|
+
* against the current content REFUSES the commit: the store has moved on,
|
|
1277
|
+
* which means an editor change landed after that parse, and committing would
|
|
1278
|
+
* overwrite a newer edit with a repair computed against text the user has
|
|
1279
|
+
* already replaced. The build that change provokes recomputes the repair.
|
|
1280
|
+
*
|
|
1281
|
+
* Deliberately NOT compared by version. A separately created document seeds
|
|
1282
|
+
* its own numbering, so requiring the two to agree would refuse every commit
|
|
1283
|
+
* on the path this exists for. Content is the thing both sides can be held to.
|
|
1284
|
+
*
|
|
1285
|
+
* A changed text is a new version authored by {@link INTEGRITY_CLIENT_ID}.
|
|
1286
|
+
* Kept at the old version, the repair is invisible to every reader keyed on
|
|
1287
|
+
* versions: a write based on the unrepaired version passes the based-on gate
|
|
1288
|
+
* and replaces the repair, and an echo filter credits the repair to the
|
|
1289
|
+
* client whose edit it corrected. No change event is fired: the repair
|
|
1290
|
+
* rides the build already under way, and an event would re-enter it.
|
|
1291
|
+
*/
|
|
1292
|
+
commitRepair(uri: DocumentUri, parsedFrom: string, repaired: string): RepairCommit<T> {
|
|
1293
|
+
const key = this.documentKey(uri);
|
|
1294
|
+
const document = this.__syncedDocuments.get(key);
|
|
1295
|
+
if (document === undefined) {
|
|
1296
|
+
return { status: 'not-open' };
|
|
1297
|
+
}
|
|
1298
|
+
if (document.getText() !== parsedFrom) {
|
|
1299
|
+
this.logUri(key, 'Refuse repair commit: the open document moved on from the text the AST was parsed from', 'debug');
|
|
1300
|
+
return { status: 'stale' };
|
|
1301
|
+
}
|
|
1302
|
+
if (repaired === parsedFrom) {
|
|
1303
|
+
return { status: 'committed', document };
|
|
1304
|
+
}
|
|
1305
|
+
// Reassigned rather than mutated in place: the default configuration
|
|
1306
|
+
// updates and returns the SAME instance, but an adopter-supplied one may
|
|
1307
|
+
// return a new object, and the store must end up holding whichever it is.
|
|
1308
|
+
const updated = this.configuration.update(document, [{ text: repaired }], document.version + 1);
|
|
1309
|
+
this.__syncedDocuments.set(key, updated);
|
|
1310
|
+
this.setAuthor(key, updated.version, INTEGRITY_CLIENT_ID);
|
|
1311
|
+
this.refreshDirty(key);
|
|
1312
|
+
this.log(updated.uri, `Update to version ${updated.version} by ${this.formatClientId(INTEGRITY_CLIENT_ID)} (repair)`);
|
|
1313
|
+
return { status: 'committed', document: updated };
|
|
1314
|
+
}
|
|
1315
|
+
|
|
835
1316
|
getAuthor(uri: DocumentUri, version?: number): string | undefined {
|
|
836
1317
|
const history = this.__documents.get(this.documentKey(uri))?.versionAuthors;
|
|
837
1318
|
// Either the requested version, or the latest. `version !== undefined` so we treat 0 correctly.
|
|
@@ -852,17 +1333,17 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
852
1333
|
* True iff any client still holds `uri` open. Distinct from {@link isOpen},
|
|
853
1334
|
* which reads `__syncedDocuments` — that map is cleared only AFTER the
|
|
854
1335
|
* `onDidClose` event fires for the last client, so `isOpen` returns `true`
|
|
855
|
-
* during the close event itself. `isOpenInAnyClient` reads the
|
|
856
|
-
*
|
|
1336
|
+
* during the close event itself. `isOpenInAnyClient` reads the open table
|
|
1337
|
+
* ({@link __sessions}), which is updated BEFORE the fire, so an `onDidClose`
|
|
857
1338
|
* subscriber that finds this `false` knows the last client just closed and a
|
|
858
1339
|
* disk re-read / rebuild can proceed.
|
|
859
1340
|
*/
|
|
860
1341
|
isOpenInAnyClient(uri: DocumentUri): boolean {
|
|
861
|
-
return
|
|
1342
|
+
return this.__sessions.isOpen(this.documentKey(uri));
|
|
862
1343
|
}
|
|
863
1344
|
|
|
864
1345
|
isOpenInClient(uri: DocumentUri, client: string): boolean {
|
|
865
|
-
return
|
|
1346
|
+
return this.__sessions.isOpenIn(this.documentKey(uri), client);
|
|
866
1347
|
}
|
|
867
1348
|
|
|
868
1349
|
isOpenInLanguageClient(uri: DocumentUri): boolean {
|
|
@@ -873,14 +1354,14 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
873
1354
|
}
|
|
874
1355
|
|
|
875
1356
|
isOnlyOpenInClient(uri: DocumentUri, client: string): boolean {
|
|
876
|
-
const
|
|
877
|
-
return
|
|
1357
|
+
const clients = this.__sessions.clientsOf(this.documentKey(uri));
|
|
1358
|
+
return clients.length === 1 && clients[0] === client;
|
|
878
1359
|
}
|
|
879
1360
|
|
|
880
1361
|
/**
|
|
881
1362
|
* Every document currently held open by at least one client, with the client
|
|
882
|
-
* ids holding it. Reads the same
|
|
883
|
-
*
|
|
1363
|
+
* ids holding it. Reads the same open table as {@link isOpenInAnyClient}, so
|
|
1364
|
+
* it reflects a last-client close immediately.
|
|
884
1365
|
*
|
|
885
1366
|
* Diagnostics-oriented — the server-state snapshot lists these so an operator
|
|
886
1367
|
* can see WHY a document is pinned: a document that lingers here after its
|
|
@@ -888,28 +1369,258 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
888
1369
|
* under a shedding policy, explains why its CST is never shed).
|
|
889
1370
|
*/
|
|
890
1371
|
openDocuments(): OpenDocument[] {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1372
|
+
return this.__sessions.openDocuments();
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/** Fires once a client session has ended, after every document it had open was closed. */
|
|
1376
|
+
get onDidCloseSession(): Event<ClientSessionClosedEvent> {
|
|
1377
|
+
return this.__sessions.onDidCloseSession;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Fires once a document no client has open is released: at its last close,
|
|
1382
|
+
* or, for a lost client's last close, when its revert grace runs out,
|
|
1383
|
+
* another client opens it, or it is deleted. The document then reverts to
|
|
1384
|
+
* disk.
|
|
1385
|
+
*/
|
|
1386
|
+
get onDidCloseLastOpen(): Event<LastOpenClosedEvent> {
|
|
1387
|
+
return this.lastOpenClosedEmitter.event;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* Fires for every save the language client reports of a document it has
|
|
1392
|
+
* open: the editor has written the file. {@link onDidSave} follows only
|
|
1393
|
+
* when the file holds the store's text.
|
|
1394
|
+
*/
|
|
1395
|
+
get onDidSaveInLanguageClient(): Event<LanguageClientSavedEvent> {
|
|
1396
|
+
return this.languageClientSavedEmitter.event;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Whether `uri` is waiting out the revert grace: its last open closed with a
|
|
1401
|
+
* lost connection, and it still holds its unsaved text. Such a document is
|
|
1402
|
+
* open for no client, yet not closed either, so a caller that would persist
|
|
1403
|
+
* a closed document's text to disk treats it as open.
|
|
1404
|
+
*/
|
|
1405
|
+
isRevertPending(uri: DocumentUri): boolean {
|
|
1406
|
+
return this.__sessions.isRevertPending(this.documentKey(uri));
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Whether the store holds `uri` with text that differs from its disk
|
|
1411
|
+
* baseline: what the server last knew the file to hold. `false` for a URI
|
|
1412
|
+
* the store does not hold; a document waiting out the revert grace is still
|
|
1413
|
+
* held.
|
|
1414
|
+
*
|
|
1415
|
+
* The baseline is the text a first open brought, or what the server wrote,
|
|
1416
|
+
* or read back after an editor's save or a watched-file change, so it can
|
|
1417
|
+
* trail a change to the file that none of these has seen yet. A check that
|
|
1418
|
+
* must know the file reads it instead.
|
|
1419
|
+
*/
|
|
1420
|
+
isDirty(uri: DocumentUri): boolean {
|
|
1421
|
+
return this.__documents.get(this.documentKey(uri))?.dirty ?? false;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/** Fires each time the answer of {@link isDirty} changes, the release of a dirty document included. */
|
|
1425
|
+
get onDidChangeDirty(): Event<DocumentDirtyChangedEvent> {
|
|
1426
|
+
return this.dirtyChangedEmitter.event;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Record that the file behind `uri` holds `text`, or no file at all for
|
|
1431
|
+
* `undefined`. A no-op for a URI the store does not hold: the next first
|
|
1432
|
+
* open sets the baseline from its own text.
|
|
1433
|
+
*/
|
|
1434
|
+
updateDiskBaseline(uri: DocumentUri, text: string | undefined): void {
|
|
1435
|
+
const key = this.documentKey(uri);
|
|
1436
|
+
const record = this.__documents.get(key);
|
|
1437
|
+
if (!record || !this.__syncedDocuments.has(key)) {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
record.diskBaseline = text;
|
|
1441
|
+
this.refreshDirty(key);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Read the file behind `uri` through its disk queue and take it as the
|
|
1446
|
+
* baseline. A file that cannot be read counts as none, the side that
|
|
1447
|
+
* leaves the document dirty.
|
|
1448
|
+
*/
|
|
1449
|
+
async reloadDiskBaseline(uri: DocumentUri): Promise<void> {
|
|
1450
|
+
const key = this.documentKey(uri);
|
|
1451
|
+
if (!this.__syncedDocuments.has(key)) {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
let onDisk: string | undefined;
|
|
1455
|
+
try {
|
|
1456
|
+
onDisk = await this.services.workspace.FileSystemTaskQueue.enqueue(key, () =>
|
|
1457
|
+
this.services.workspace.FileSystemProvider.readFile(UriUtils.toUri(key))
|
|
1458
|
+
);
|
|
1459
|
+
} catch (err: unknown) {
|
|
1460
|
+
this.tracer.with(key).debug(`Disk baseline: the file cannot be read. ${err instanceof Error ? err.message : String(err)}`);
|
|
1461
|
+
}
|
|
1462
|
+
this.updateDiskBaseline(key, onDisk);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/** Compare the held text of `uri` with its baseline, and announce a changed answer. */
|
|
1466
|
+
protected refreshDirty(uri: CanonicalUri): void {
|
|
1467
|
+
const record = this.__documents.get(uri);
|
|
1468
|
+
const document = this.__syncedDocuments.get(uri);
|
|
1469
|
+
if (!record || !document) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
const dirty = document.getText() !== record.diskBaseline;
|
|
1473
|
+
if (dirty !== (record.dirty ?? false)) {
|
|
1474
|
+
record.dirty = dirty;
|
|
1475
|
+
this.dirtyChangedEmitter.fire(Object.freeze({ uri, dirty }));
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Start a client session under `clientId`. Throws where
|
|
1481
|
+
* {@link ClientSessionRegistry.register} refuses the id.
|
|
1482
|
+
*/
|
|
1483
|
+
registerSession(clientId: string): void {
|
|
1484
|
+
this.__sessions.register(clientId);
|
|
1485
|
+
this.tracer.info(`Session started: ${this.formatClientId(clientId)}`);
|
|
1486
|
+
this.tracer.trace(`Session started: ${clientId}`);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* End the client session `clientId`: close every document it has open, then
|
|
1491
|
+
* free the id. Immediate — each close runs the ordinary close path before
|
|
1492
|
+
* this returns, with `cause` as its cause. A no-op for an id that is not a
|
|
1493
|
+
* registered session.
|
|
1494
|
+
*/
|
|
1495
|
+
closeSession(clientId: string, cause: SessionEndCause = 'closed'): void {
|
|
1496
|
+
if (!this.__sessions.isRegistered(clientId)) {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
try {
|
|
1500
|
+
for (const uri of this.__sessions.beginClose(clientId)) {
|
|
1501
|
+
this.notifyDidCloseTextDocument({ textDocument: { uri } }, clientId, cause);
|
|
1502
|
+
}
|
|
1503
|
+
} finally {
|
|
1504
|
+
this.__sessions.unregister(clientId);
|
|
1505
|
+
this.tracer.info(`Session closed: ${this.formatClientId(clientId)}`);
|
|
1506
|
+
this.tracer.trace(`Session closed: ${clientId}`);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Close every document the language client has open, as a `didClose` for
|
|
1512
|
+
* each would, so each last close reverts. For a host whose editor connection
|
|
1513
|
+
* can end while the process lives on, such as a worker whose port's peer
|
|
1514
|
+
* closed: the language client is no session, so nothing else closes them.
|
|
1515
|
+
*
|
|
1516
|
+
* It first waits for the workspace initialization the open handler waits
|
|
1517
|
+
* for, so an open that arrived before the close is closed too; closing at
|
|
1518
|
+
* once would leave that open to land afterwards, held by a client that is
|
|
1519
|
+
* gone.
|
|
1520
|
+
*/
|
|
1521
|
+
async closeLanguageClientDocuments(): Promise<void> {
|
|
1522
|
+
await this.initialBuildFinished();
|
|
1523
|
+
for (const uri of this.__sessions.opensOf(LANGUAGE_CLIENT_ID)) {
|
|
1524
|
+
this.untrackLanguageClientDocuments(uri);
|
|
1525
|
+
this.notifyDidCloseTextDocument({ textDocument: { uri } });
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Settles once the initial workspace build has finished, whether it
|
|
1531
|
+
* completed, was cancelled or failed. Awaiting `workspaceInitialized` itself
|
|
1532
|
+
* throws after a cancelled build, which any write during startup causes, and
|
|
1533
|
+
* every open and close gated on it is then dropped for the process lifetime.
|
|
1534
|
+
* The workspace manager logs a failed build.
|
|
1535
|
+
*/
|
|
1536
|
+
protected async initialBuildFinished(): Promise<void> {
|
|
1537
|
+
await this.services.workspace.WorkspaceManager.workspaceInitialized.catch(() => undefined);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Stop tracking every URI the language client holds the document `key`
|
|
1542
|
+
* under, with its shadow and pending pushes. Call before a close by
|
|
1543
|
+
* canonical key that means all of them: a close for one URI while others
|
|
1544
|
+
* remain drops only that URI's and keeps the client's hold.
|
|
1545
|
+
*/
|
|
1546
|
+
protected untrackLanguageClientDocuments(key: CanonicalUri): void {
|
|
1547
|
+
const languageClientDocuments = this.__documents.get(key)?.languageClientDocuments;
|
|
1548
|
+
for (const clientUri of languageClientDocuments?.keys() ?? []) {
|
|
1549
|
+
this.__shadow.invalidate(clientUri);
|
|
1550
|
+
this.__pendingPushes.delete(clientUri);
|
|
1551
|
+
}
|
|
1552
|
+
languageClientDocuments?.clear();
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* The file behind `uri` was deleted: close every open of it except the
|
|
1557
|
+
* language client's.
|
|
1558
|
+
*
|
|
1559
|
+
* The editor's open is left alone because the editor owns it: it keeps the
|
|
1560
|
+
* buffer of a deleted file and goes on sending changes for it, and a store
|
|
1561
|
+
* that had closed the document would drop every one of them until the editor
|
|
1562
|
+
* reopened.
|
|
1563
|
+
*/
|
|
1564
|
+
notifyDocumentDeleted(uri: DocumentUri): void {
|
|
1565
|
+
const key = this.documentKey(uri);
|
|
1566
|
+
for (const clientId of this.__sessions.clientsOf(key)) {
|
|
1567
|
+
if (clientId !== LANGUAGE_CLIENT_ID) {
|
|
1568
|
+
this.notifyDidCloseTextDocument({ textDocument: { uri: key } }, clientId);
|
|
895
1569
|
}
|
|
896
1570
|
}
|
|
897
|
-
return result;
|
|
898
1571
|
}
|
|
899
1572
|
|
|
900
1573
|
/**
|
|
901
|
-
*
|
|
1574
|
+
* Remove the document for `uri`, closing every open of it first — the
|
|
1575
|
+
* editor's included, since the document it would keep editing is gone.
|
|
1576
|
+
*
|
|
1577
|
+
* The base removes the synced document without consulting the open table,
|
|
1578
|
+
* which leaves every open recorded against a document that no longer exists:
|
|
1579
|
+
* the store then answers that the URI is open, and the next open attaches to
|
|
1580
|
+
* nothing.
|
|
1581
|
+
*/
|
|
1582
|
+
override delete(uri: string | URI | T): void {
|
|
1583
|
+
const key = this.documentKey((typeof uri === 'object' && 'uri' in uri ? uri.uri : uri).toString());
|
|
1584
|
+
this.untrackLanguageClientDocuments(key);
|
|
1585
|
+
for (const clientId of this.__sessions.clientsOf(key)) {
|
|
1586
|
+
this.notifyDidCloseTextDocument({ textDocument: { uri: key } }, clientId);
|
|
1587
|
+
}
|
|
1588
|
+
// A document waiting out the grace has no client left to close, and is
|
|
1589
|
+
// released now as its last close would have released it.
|
|
1590
|
+
if (this.__sessions.isRevertPending(key)) {
|
|
1591
|
+
this.__sessions.cancelRevert(key);
|
|
1592
|
+
this.releaseDocument(key);
|
|
1593
|
+
}
|
|
1594
|
+
super.delete(key);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* A client id as log lines print it. An id containing `#` is cut eight
|
|
1599
|
+
* characters after its last `#`, which leaves a minted `label#uuid` as the
|
|
1600
|
+
* label and enough of the UUID to tell sessions apart; an id without `#`
|
|
1601
|
+
* prints whole. The full id goes out at trace level wherever a session
|
|
1602
|
+
* starts or ends.
|
|
1603
|
+
*/
|
|
1604
|
+
protected formatClientId(clientId: string): string {
|
|
1605
|
+
const separator = clientId.lastIndexOf('#');
|
|
1606
|
+
return separator < 0 ? clientId : clientId.slice(0, separator + 9);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Stages integrity-updated content for a document no client holds.
|
|
902
1611
|
*
|
|
903
1612
|
* When `workspace/applyEdit` targets a closed file, the client opens it from
|
|
904
1613
|
* disk (stale) and sends `didOpen` before applying the edit. This staged
|
|
905
1614
|
* content is consumed by {@link notifyDidOpenTextDocument} to replace the
|
|
906
1615
|
* stale disk text, preventing a brief revert of the integrity update.
|
|
907
1616
|
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
*
|
|
1617
|
+
* Only a FIRST open consumes it, so stage only for a URI no client holds
|
|
1618
|
+
* ({@link isOpenInAnyClient} is `false`). A URI held only through another
|
|
1619
|
+
* head is not closed: an editor attaching to it joins the existing entry and
|
|
1620
|
+
* never reads the stage, and the last close discards the stage with the
|
|
1621
|
+
* tracking record. An entry lingers only if `workspace/applyEdit` fails and
|
|
1622
|
+
* the file is never opened — the memory cost is one serialised string per
|
|
1623
|
+
* URI.
|
|
913
1624
|
*/
|
|
914
1625
|
stagePendingContent(uri: DocumentUri, text: string): void {
|
|
915
1626
|
this.trackingFor(this.documentKey(uri)).pendingContent = text;
|
|
@@ -942,8 +1653,9 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
942
1653
|
* and the client applying, the ranges address the wrong lines and splice the
|
|
943
1654
|
* buffer (observed as a duplicated declaration, which the integrity tier then
|
|
944
1655
|
* "repairs" into a suffixed name and persists). The edit is therefore
|
|
945
|
-
* addressed at the language client's
|
|
946
|
-
* `null` ("version
|
|
1656
|
+
* addressed at the language client's last known version for that URI (see
|
|
1657
|
+
* {@link languageClientVersion}) rather than at `null` ("version
|
|
1658
|
+
* intentionally unknown"), which is what lets the client
|
|
947
1659
|
* reject a push its buffer has outrun. On rejection the shadow is invalidated,
|
|
948
1660
|
* so the caller's retry is a full-range replace — position-independent, and
|
|
949
1661
|
* safe to apply to whatever the client now holds.
|
|
@@ -963,14 +1675,15 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
963
1675
|
// when the same file was opened under a symlink and its real path. Falls back
|
|
964
1676
|
// to the normalized URI when nothing is tracked. The shadow is keyed by each
|
|
965
1677
|
// URI, so every diff is against the right baseline.
|
|
966
|
-
const recorded = this.__documents.get(this.documentKey(uri))?.
|
|
967
|
-
const targets: Iterable<LanguageClientUri> = recorded && recorded.size > 0 ? recorded : [this.toLanguageClientUri(uri)];
|
|
1678
|
+
const recorded = this.__documents.get(this.documentKey(uri))?.languageClientDocuments;
|
|
1679
|
+
const targets: Iterable<LanguageClientUri> = recorded && recorded.size > 0 ? [...recorded.keys()] : [this.toLanguageClientUri(uri)];
|
|
968
1680
|
let lastResult: ApplyWorkspaceEditResult | undefined;
|
|
969
1681
|
for (const targetUri of targets) {
|
|
970
|
-
// Read BEFORE computeEdits, which overwrites
|
|
971
|
-
//
|
|
972
|
-
// echo of
|
|
973
|
-
|
|
1682
|
+
// Read BEFORE computeEdits, which overwrites the baseline and drops
|
|
1683
|
+
// the opened snapshot. This is the text the client holds, and
|
|
1684
|
+
// therefore the only text its echo of this push can be reconstructed
|
|
1685
|
+
// against — whether or not the push below is keyed to it.
|
|
1686
|
+
const before = this.__shadow.clientText(targetUri);
|
|
974
1687
|
const edits = this.__shadow.computeEdits(targetUri, newText);
|
|
975
1688
|
if (edits.length === 0) {
|
|
976
1689
|
continue;
|
|
@@ -984,14 +1697,42 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
984
1697
|
// stale-by-one version into a refused update for no safety gain — and
|
|
985
1698
|
// it is exactly what the caller retries with after a rejection, so
|
|
986
1699
|
// gating it there would refuse the recovery too.
|
|
987
|
-
const version = isFullReplace(edits) ? UNKNOWN_CLIENT_VERSION : this.languageClientVersion(uri);
|
|
1700
|
+
const version = isFullReplace(edits) ? UNKNOWN_CLIENT_VERSION : this.languageClientVersion(uri, targetUri);
|
|
1701
|
+
// Captured before the await: a reopen replaces the state, so a late
|
|
1702
|
+
// reply then writes to the old one instead of the new buffer's.
|
|
1703
|
+
const languageClientDocument = recorded?.get(targetUri);
|
|
1704
|
+
// A full `ApplyWorkspaceEditParams`, `edit` and all — NOT a bare
|
|
1705
|
+
// `WorkspaceEdit` with a `label` beside it. `applyEdit` takes
|
|
1706
|
+
// `ApplyWorkspaceEditParams | WorkspaceEdit` and discriminates on
|
|
1707
|
+
// `!!value.edit`, so `{ label, documentChanges }` is wrapped as
|
|
1708
|
+
// `{ edit: { label, documentChanges } }` — putting the label inside
|
|
1709
|
+
// the edit, where LSP defines no such field and no client reads it.
|
|
1710
|
+
// The union is also what hides it at compile time: excess-property
|
|
1711
|
+
// checking admits a property present in EITHER member, so an object
|
|
1712
|
+
// matching neither type-checks against the union.
|
|
988
1713
|
const result = await connection.workspace.applyEdit({
|
|
989
1714
|
label: options?.label,
|
|
990
|
-
|
|
1715
|
+
edit: {
|
|
1716
|
+
documentChanges: [TextDocumentEdit.create(OptionalVersionedTextDocumentIdentifier.create(targetUri, version), edits)]
|
|
1717
|
+
}
|
|
991
1718
|
});
|
|
992
1719
|
if (result && result.applied === false) {
|
|
993
1720
|
this.__shadow.invalidate(targetUri);
|
|
994
1721
|
this.__pendingPushes.delete(targetUri);
|
|
1722
|
+
if (version !== UNKNOWN_CLIENT_VERSION) {
|
|
1723
|
+
this.tracer
|
|
1724
|
+
.with(uri)
|
|
1725
|
+
.warn(
|
|
1726
|
+
`Language client refused applyEdit addressed at version ${version} (it last declared version ${languageClientDocument?.declaredVersion})`
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
} else if (result?.applied && version !== UNKNOWN_CLIENT_VERSION && languageClientDocument) {
|
|
1730
|
+
// A client steps once per applied edit that changes its buffer. A
|
|
1731
|
+
// line edit does while the shadow is right; a full replace may be a
|
|
1732
|
+
// no-op the client drops. Left to the echo, a push sent first is
|
|
1733
|
+
// refused; advanced after a no-op, one could pass at a version a
|
|
1734
|
+
// keystroke reached.
|
|
1735
|
+
languageClientDocument.pushedVersion = version + 1;
|
|
995
1736
|
}
|
|
996
1737
|
lastResult = result;
|
|
997
1738
|
} catch (err) {
|
|
@@ -1004,21 +1745,27 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
1004
1745
|
}
|
|
1005
1746
|
|
|
1006
1747
|
/**
|
|
1007
|
-
* The version the LSP textual language client
|
|
1008
|
-
* addressing an outgoing `workspace/applyEdit`.
|
|
1748
|
+
* The version the LSP textual language client holds `uri` at under
|
|
1749
|
+
* `targetUri`, for addressing an outgoing `workspace/applyEdit`.
|
|
1009
1750
|
*
|
|
1010
1751
|
* Client version ids are CLIENT-owned per LSP, so this is the id the client
|
|
1011
|
-
* itself stamped on its last `didOpen` / `didChange`
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1752
|
+
* itself stamped on its last `didOpen` / `didChange` for `targetUri`, or the
|
|
1753
|
+
* one an applied push moved it to ahead of the push's echo
|
|
1754
|
+
* ({@link LanguageClientDocumentState}) — never the shared server version,
|
|
1755
|
+
* which advances on authored writes the client knows nothing about and would
|
|
1756
|
+
* therefore reject every push.
|
|
1014
1757
|
*
|
|
1015
|
-
* Falls back to {@link UNKNOWN_CLIENT_VERSION} when the client has
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1758
|
+
* Falls back to {@link UNKNOWN_CLIENT_VERSION} when the client has not opened
|
|
1759
|
+
* the document under `targetUri`, which is the honest answer. That is also
|
|
1760
|
+
* the case in which there is no shadow, so the push is already a
|
|
1018
1761
|
* position-independent full replace and has nothing to gain from a gate.
|
|
1019
1762
|
*/
|
|
1020
|
-
protected languageClientVersion(uri: DocumentUri): number {
|
|
1021
|
-
|
|
1763
|
+
protected languageClientVersion(uri: DocumentUri, targetUri: LanguageClientUri = this.toLanguageClientUri(uri)): number {
|
|
1764
|
+
const state = this.__documents.get(this.documentKey(uri))?.languageClientDocuments?.get(targetUri);
|
|
1765
|
+
if (state === undefined) {
|
|
1766
|
+
return UNKNOWN_CLIENT_VERSION;
|
|
1767
|
+
}
|
|
1768
|
+
return Math.max(state.declaredVersion, state.pushedVersion ?? state.declaredVersion);
|
|
1022
1769
|
}
|
|
1023
1770
|
|
|
1024
1771
|
/**
|
|
@@ -1034,7 +1781,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
1034
1781
|
pending = [];
|
|
1035
1782
|
this.__pendingPushes.set(targetUri, pending);
|
|
1036
1783
|
}
|
|
1037
|
-
pending.push({ before, afterHash:
|
|
1784
|
+
pending.push({ before, afterHash: textHash(newText) });
|
|
1038
1785
|
if (pending.length > PENDING_ECHO_CAP) {
|
|
1039
1786
|
pending.shift();
|
|
1040
1787
|
this.logUri(targetUri, `Pending-echo queue exceeded ${PENDING_ECHO_CAP} entries; dropped the oldest`, 'debug');
|
|
@@ -1043,12 +1790,17 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
1043
1790
|
|
|
1044
1791
|
/**
|
|
1045
1792
|
* Decide what an incoming language-client change actually is, by
|
|
1046
|
-
* reconstructing the client's resulting buffer against the
|
|
1047
|
-
* in flight for
|
|
1793
|
+
* reconstructing the client's resulting buffer against the text its ranges
|
|
1794
|
+
* address — the pre-push buffer of the OLDEST push still in flight for
|
|
1795
|
+
* `clientFacing`, else whatever that client is believed to hold.
|
|
1048
1796
|
*
|
|
1049
|
-
* `undefined` when
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1797
|
+
* `undefined` when the client's ranges address the synced text itself —
|
|
1798
|
+
* nothing in flight, and no evidence the client holds anything else. That
|
|
1799
|
+
* is the ordinary path, and the caller then applies the ranges directly.
|
|
1800
|
+
* The two texts part company without a push in flight whenever a client
|
|
1801
|
+
* attaches to a document another client has already written: it opened
|
|
1802
|
+
* from disk, so applying its ranges to the synced text splices lines they
|
|
1803
|
+
* never addressed.
|
|
1052
1804
|
*
|
|
1053
1805
|
* **Why the oldest, and why reconstruct at all.** The client applies our
|
|
1054
1806
|
* pushes in order and echoes each against the buffer it held before that
|
|
@@ -1079,22 +1831,41 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
1079
1831
|
document: T,
|
|
1080
1832
|
changes: TextDocumentContentChangeEvent[]
|
|
1081
1833
|
): LanguageClientChangeOrigin | undefined {
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1834
|
+
const queued = this.__pendingPushes.get(clientFacing);
|
|
1835
|
+
const pending = queued?.length ? queued : undefined;
|
|
1836
|
+
// The text the client's ranges address: the buffer it held before the
|
|
1837
|
+
// oldest push still in flight, else the buffer it is believed to hold.
|
|
1838
|
+
const clientText = pending?.[0].before ?? this.__shadow.clientText(clientFacing);
|
|
1839
|
+
if (pending === undefined && (clientText === undefined || clientText === document.getText())) {
|
|
1084
1840
|
return undefined;
|
|
1085
1841
|
}
|
|
1842
|
+
if (pending !== undefined && pending[0].before === undefined && changes.some(change => ContentChange.isIncremental(change))) {
|
|
1843
|
+
// A push sent to a client whose buffer was unknown — a first sync, or
|
|
1844
|
+
// the recovery push after a rejection. The shadow now holds the text
|
|
1845
|
+
// that push MOVES the client to, which is the one text the echo's
|
|
1846
|
+
// ranges provably do not address, so the fallback above is a baseline
|
|
1847
|
+
// known to be wrong rather than merely unverified. Drop the queue and
|
|
1848
|
+
// the shadow: the next sync is then a full replace, which lands on
|
|
1849
|
+
// whatever the client holds. A full-text change is exempt because it
|
|
1850
|
+
// reconstructs identically against any baseline.
|
|
1851
|
+
pending.length = 0;
|
|
1852
|
+
this.__shadow.invalidate(clientFacing);
|
|
1853
|
+
return { kind: 'unreconstructable' };
|
|
1854
|
+
}
|
|
1086
1855
|
// Through the configured factories, not `TextDocument` directly, so an
|
|
1087
1856
|
// adopter's custom text-document type governs how the ranges are applied
|
|
1088
1857
|
// here exactly as it does on the synced document.
|
|
1089
|
-
const probe = this.create(clientFacing, document.languageId, 0,
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1858
|
+
const probe = this.create(clientFacing, document.languageId, 0, clientText ?? document.getText());
|
|
1859
|
+
const reconstructed = this.update(probe, changes, 0).getText();
|
|
1860
|
+
if (pending !== undefined) {
|
|
1861
|
+
const matchIndex = pending.findIndex(push => push.afterHash === textHash(reconstructed));
|
|
1862
|
+
if (matchIndex >= 0) {
|
|
1863
|
+
pending.splice(0, matchIndex + 1);
|
|
1864
|
+
return { kind: 'echo' };
|
|
1865
|
+
}
|
|
1866
|
+
pending.length = 0;
|
|
1095
1867
|
}
|
|
1096
|
-
|
|
1097
|
-
return { kind: 'divergent', text: clientText };
|
|
1868
|
+
return { kind: 'divergent', text: reconstructed };
|
|
1098
1869
|
}
|
|
1099
1870
|
|
|
1100
1871
|
/**
|