@hydranium/core 1.0.0-next.8 → 1.0.0-next.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/documents/ast-document-manager.d.ts +75 -16
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +57 -19
- 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/hydranium-text-documents.d.ts +55 -21
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +142 -46
- package/lib/documents/hydranium-text-documents.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 +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -0
- package/lib/index.js.map +1 -1
- package/lib/langium/ast-extension/ast-node-builder.d.ts +32 -9
- package/lib/langium/ast-extension/ast-node-builder.d.ts.map +1 -1
- package/lib/langium/ast-extension/ast-node-builder.js +60 -22
- package/lib/langium/ast-extension/ast-node-builder.js.map +1 -1
- package/lib/langium/bootstrap.d.ts.map +1 -1
- package/lib/langium/bootstrap.js +16 -9
- package/lib/langium/bootstrap.js.map +1 -1
- package/lib/langium/config/configuration-provider.d.ts +81 -0
- package/lib/langium/config/configuration-provider.d.ts.map +1 -0
- package/lib/langium/config/configuration-provider.js +112 -0
- package/lib/langium/config/configuration-provider.js.map +1 -0
- package/lib/langium/config/index.d.ts +1 -0
- package/lib/langium/config/index.d.ts.map +1 -1
- package/lib/langium/config/index.js +1 -0
- package/lib/langium/config/index.js.map +1 -1
- package/lib/langium/diagnostics/lsp-logger.d.ts +17 -0
- package/lib/langium/diagnostics/lsp-logger.d.ts.map +1 -1
- package/lib/langium/diagnostics/lsp-logger.js +40 -6
- package/lib/langium/diagnostics/lsp-logger.js.map +1 -1
- package/lib/langium/document-builder/build-pipeline-integration.d.ts +13 -1
- package/lib/langium/document-builder/build-pipeline-integration.d.ts.map +1 -1
- package/lib/langium/document-builder/build-pipeline-integration.js +1 -1
- 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 +150 -7
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +298 -11
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/document-builder/index.d.ts +1 -0
- package/lib/langium/document-builder/index.d.ts.map +1 -1
- package/lib/langium/document-builder/index.js +1 -0
- package/lib/langium/document-builder/index.js.map +1 -1
- package/lib/langium/integration-services.d.ts +45 -1
- package/lib/langium/integration-services.d.ts.map +1 -1
- package/lib/langium/integration-services.js +10 -5
- package/lib/langium/integration-services.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +5 -0
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +34 -1
- 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/model-service/model-service.d.ts +149 -10
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +120 -93
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +84 -15
- package/lib/langium/module.d.ts.map +1 -1
- package/lib/langium/module.js +26 -12
- package/lib/langium/module.js.map +1 -1
- package/lib/langium/naming/name-provider.d.ts +25 -4
- package/lib/langium/naming/name-provider.d.ts.map +1 -1
- package/lib/langium/naming/name-provider.js +2 -1
- package/lib/langium/naming/name-provider.js.map +1 -1
- package/lib/langium/naming/name-separator-validation.d.ts +23 -0
- package/lib/langium/naming/name-separator-validation.d.ts.map +1 -1
- package/lib/langium/naming/name-separator-validation.js +30 -1
- package/lib/langium/naming/name-separator-validation.js.map +1 -1
- package/lib/langium/residency/cst-residency-service.d.ts +31 -9
- package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
- package/lib/langium/residency/cst-residency-service.js +13 -56
- package/lib/langium/residency/cst-residency-service.js.map +1 -1
- package/lib/langium/scope/hydranium-scope-provider.d.ts +32 -13
- package/lib/langium/scope/hydranium-scope-provider.d.ts.map +1 -1
- package/lib/langium/scope/hydranium-scope-provider.js +37 -20
- package/lib/langium/scope/hydranium-scope-provider.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/validation/document-validator.d.ts +294 -10
- package/lib/langium/validation/document-validator.d.ts.map +1 -1
- package/lib/langium/validation/document-validator.js +448 -5
- package/lib/langium/validation/document-validator.js.map +1 -1
- package/lib/langium/workspace/document-uri-policy.d.ts +3 -4
- package/lib/langium/workspace/document-uri-policy.d.ts.map +1 -1
- package/lib/langium/workspace/document-uri-policy.js +3 -4
- package/lib/langium/workspace/document-uri-policy.js.map +1 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.d.ts +25 -0
- package/lib/langium/workspace/hydranium-langium-document-factory.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.js +25 -0
- package/lib/langium/workspace/hydranium-langium-document-factory.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts +21 -1
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.js +28 -0
- package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts +8 -0
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.js +11 -2
- package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
- package/lib/langium/workspace/initialize-workspace.d.ts +22 -2
- package/lib/langium/workspace/initialize-workspace.d.ts.map +1 -1
- package/lib/langium/workspace/initialize-workspace.js +13 -5
- package/lib/langium/workspace/initialize-workspace.js.map +1 -1
- package/lib/langium/workspace/langium-documents.d.ts +79 -17
- package/lib/langium/workspace/langium-documents.d.ts.map +1 -1
- package/lib/langium/workspace/langium-documents.js +83 -28
- package/lib/langium/workspace/langium-documents.js.map +1 -1
- package/lib/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/hydranium-document-update-handler.js +1 -1
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/lsp/index.d.ts +1 -1
- package/lib/lsp/index.d.ts.map +1 -1
- package/lib/lsp/index.js +1 -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/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 +136 -0
- package/lib/messages/renderer.d.ts.map +1 -0
- package/lib/messages/renderer.js +159 -0
- package/lib/messages/renderer.js.map +1 -0
- package/lib/node/heap-ceiling.d.ts +61 -0
- package/lib/node/heap-ceiling.d.ts.map +1 -0
- package/lib/node/heap-ceiling.js +72 -0
- package/lib/node/heap-ceiling.js.map +1 -0
- package/lib/node/index.d.ts +1 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +1 -0
- package/lib/node/index.js.map +1 -1
- package/lib/node/latency-from-env.d.ts +1 -1
- package/lib/node/latency-from-env.js +1 -1
- package/lib/node/node-file-system-provider.d.ts.map +1 -1
- package/lib/node/node-file-system-provider.js +1 -40
- package/lib/node/node-file-system-provider.js.map +1 -1
- package/lib/node/profiling-run.d.ts +2 -2
- package/lib/node/profiling-run.js +1 -1
- package/lib/node/profiling-run.js.map +1 -1
- package/lib/node/rename-over-open-readers.d.ts +52 -0
- package/lib/node/rename-over-open-readers.d.ts.map +1 -0
- package/lib/node/rename-over-open-readers.js +66 -0
- package/lib/node/rename-over-open-readers.js.map +1 -0
- package/lib/testing/fake-document.d.ts +16 -4
- package/lib/testing/fake-document.d.ts.map +1 -1
- package/lib/testing/fake-document.js +18 -3
- package/lib/testing/fake-document.js.map +1 -1
- package/lib/testing/make-noop-shared-services.d.ts +14 -0
- package/lib/testing/make-noop-shared-services.d.ts.map +1 -1
- package/lib/testing/make-noop-shared-services.js +28 -2
- package/lib/testing/make-noop-shared-services.js.map +1 -1
- package/lib/testing/make-test-services.d.ts +44 -8
- package/lib/testing/make-test-services.d.ts.map +1 -1
- package/lib/testing/make-test-services.js +19 -3
- package/lib/testing/make-test-services.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/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/stub-ast-document-manager.d.ts +5 -5
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +4 -6
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-document-builder.d.ts +8 -0
- package/lib/testing/stub-document-builder.d.ts.map +1 -1
- package/lib/testing/stub-document-builder.js +22 -3
- package/lib/testing/stub-document-builder.js.map +1 -1
- package/lib/testing/stub-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 +5 -4
- package/lib/testing/stub-model-service.d.ts.map +1 -1
- package/lib/testing/stub-model-service.js +2 -2
- package/lib/testing/stub-model-service.js.map +1 -1
- package/package.json +17 -8
- package/src/documents/ast-document-manager.ts +129 -36
- package/src/documents/client-ids.ts +5 -22
- package/src/documents/hydranium-text-documents.ts +167 -61
- package/src/documents/language-client-text-shadow.ts +59 -11
- package/src/documents/self-save-registry.ts +52 -13
- package/src/index.ts +5 -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 +14 -1
- package/src/langium/document-builder/build-session.ts +87 -0
- package/src/langium/document-builder/document-builder.ts +339 -11
- package/src/langium/document-builder/index.ts +1 -0
- package/src/langium/integration-services.ts +67 -6
- package/src/langium/integrity/integrity-service.ts +35 -1
- 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/model-service/model-service.ts +211 -30
- package/src/langium/module.ts +106 -21
- package/src/langium/naming/name-provider.ts +27 -5
- package/src/langium/naming/name-separator-validation.ts +34 -5
- package/src/langium/residency/cst-residency-service.ts +36 -12
- package/src/langium/scope/hydranium-scope-provider.ts +40 -22
- package/src/langium/shared-services.ts +19 -2
- package/src/langium/transfer/transfer-encoder.ts +85 -24
- package/src/langium/validation/document-validator.ts +537 -11
- package/src/langium/workspace/document-uri-policy.ts +3 -4
- package/src/langium/workspace/hydranium-langium-document-factory.ts +34 -0
- package/src/langium/workspace/hydranium-workspace-manager.ts +30 -1
- package/src/langium/workspace/in-memory-file-system-provider.ts +13 -3
- package/src/langium/workspace/initialize-workspace.ts +34 -5
- package/src/langium/workspace/langium-documents.ts +106 -33
- package/src/locale/index.ts +10 -0
- package/src/locale/server-locale.ts +86 -0
- package/src/lsp/hydranium-document-update-handler.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/lsp-latency.ts +62 -0
- package/src/lsp/semantic-token-provider.ts +74 -7
- package/src/messages/carriers.ts +78 -0
- package/src/messages/index.ts +36 -0
- package/src/messages/renderer.ts +204 -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/node-file-system-provider.ts +1 -41
- package/src/node/profiling-run.ts +3 -3
- package/src/node/rename-over-open-readers.ts +78 -0
- package/src/testing/fake-document.ts +24 -5
- package/src/testing/make-noop-shared-services.ts +50 -2
- package/src/testing/make-test-services.ts +73 -18
- package/src/testing/node/spawned-server.ts +35 -1
- 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/stub-ast-document-manager.ts +10 -14
- package/src/testing/stub-document-builder.ts +32 -3
- package/src/testing/stub-langium-documents.ts +3 -2
- package/src/testing/stub-model-service.ts +7 -6
- 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
|
@@ -37,6 +37,7 @@ import {
|
|
|
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,
|
|
@@ -165,15 +166,15 @@ interface ConnectionWithTextDocumentSync {
|
|
|
165
166
|
*/
|
|
166
167
|
interface PendingPush {
|
|
167
168
|
/**
|
|
168
|
-
* The text the client held BEFORE this push
|
|
169
|
-
*
|
|
170
|
-
*
|
|
169
|
+
* The text the client held BEFORE this push, and therefore the text its
|
|
170
|
+
* echo addresses with its ranges. Wider than the baseline the push's edits
|
|
171
|
+
* were diffed against: a full replace is sent with no diff baseline and
|
|
172
|
+
* still lands on a buffer the echo is keyed to.
|
|
171
173
|
*
|
|
172
|
-
* `undefined` when
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* application is position-independent.
|
|
174
|
+
* `undefined` only when that buffer is unknown — the client never declared
|
|
175
|
+
* one, or a rejection invalidated what was tracked. The echo is then
|
|
176
|
+
* reconstructed against the synced text, which is sound only for a
|
|
177
|
+
* position-independent (full-text) change.
|
|
177
178
|
*/
|
|
178
179
|
readonly before: string | undefined;
|
|
179
180
|
/** {@link contentHash} of the text this push moves the client to. */
|
|
@@ -182,18 +183,25 @@ interface PendingPush {
|
|
|
182
183
|
|
|
183
184
|
/**
|
|
184
185
|
* What an incoming language-client change turns out to be once reconstructed
|
|
185
|
-
* against the
|
|
186
|
+
* against the buffer its ranges address — the return of
|
|
186
187
|
* {@link HydraniumTextDocuments.classifyLanguageClientChange}.
|
|
187
188
|
*/
|
|
188
189
|
type LanguageClientChangeOrigin =
|
|
189
190
|
/** The client is reporting a text we pushed it. The synced document is already there. */
|
|
190
191
|
| { readonly kind: 'echo' }
|
|
191
192
|
/**
|
|
192
|
-
* The client's buffer
|
|
193
|
-
* a
|
|
194
|
-
* reconstructed text is what it now holds, and is authoritative.
|
|
193
|
+
* The client's buffer holds a text we did not push it — a keystroke that
|
|
194
|
+
* raced a push, or an edit to a buffer the store has already been written
|
|
195
|
+
* past. The reconstructed text is what it now holds, and is authoritative.
|
|
195
196
|
*/
|
|
196
|
-
| { readonly kind: 'divergent'; readonly text: string }
|
|
197
|
+
| { readonly kind: 'divergent'; readonly text: string }
|
|
198
|
+
/**
|
|
199
|
+
* The change carries ranges and no known text addresses them, so no
|
|
200
|
+
* reconstruction is offered. Adopting one anyway splices the document and
|
|
201
|
+
* stores an edit nobody made; dropping costs at most the one keystroke the
|
|
202
|
+
* client still holds and the next push contradicts.
|
|
203
|
+
*/
|
|
204
|
+
| { readonly kind: 'unreconstructable' };
|
|
197
205
|
|
|
198
206
|
/**
|
|
199
207
|
* Cheap, stable, non-cryptographic content hash (cyrb53) for the version
|
|
@@ -229,7 +237,7 @@ function contentHash(text: string): string {
|
|
|
229
237
|
* per-client staleness guard and never leak into the shared sequence —
|
|
230
238
|
* the two are different things (an editor's edit-operation counter vs the
|
|
231
239
|
* document's content-revision number), and splicing them lets versions drift
|
|
232
|
-
* silently past
|
|
240
|
+
* silently past based-on gate holders.
|
|
233
241
|
* - Version-author history so each edit is attributable to its originating client.
|
|
234
242
|
* - Pending-content staging used by the integrity service to thread corrections
|
|
235
243
|
* through `workspace/applyEdit` cycles for currently-closed documents.
|
|
@@ -256,7 +264,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
256
264
|
* that record is deleted on last close, while the version sequence must
|
|
257
265
|
* survive it — the shared version is a server-owned, monotonic,
|
|
258
266
|
* advances-iff-content-changes counter that never resets while the server
|
|
259
|
-
* lives. That invariant is what makes an optimistic
|
|
267
|
+
* lives. That invariant is what makes an optimistic based-on gate
|
|
260
268
|
* sound: "version unchanged ⇔ content unchanged", with no false conflicts
|
|
261
269
|
* from close/reopen version resets and no false passes from a reopened
|
|
262
270
|
* sequence coincidentally landing on a stale writer's number.
|
|
@@ -471,15 +479,16 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
471
479
|
}
|
|
472
480
|
record.clientVersions.set(clientId, td.version);
|
|
473
481
|
|
|
474
|
-
// A language-client change
|
|
475
|
-
//
|
|
476
|
-
//
|
|
482
|
+
// A language-client change is keyed to the buffer that client holds,
|
|
483
|
+
// which is the synced text only while the two agree — an authored
|
|
484
|
+
// write advances the synced text without the client knowing, and a
|
|
485
|
+
// push in flight moves the client without the store knowing.
|
|
477
486
|
// Resolve which it is, and against what, before touching anything.
|
|
478
|
-
const
|
|
487
|
+
const origin =
|
|
479
488
|
clientId === LANGUAGE_CLIENT_ID
|
|
480
489
|
? this.classifyLanguageClientChange(this.toLanguageClientUri(td.uri), document, changes)
|
|
481
490
|
: undefined;
|
|
482
|
-
if (
|
|
491
|
+
if (origin?.kind === 'echo') {
|
|
483
492
|
// The client is reporting a text we pushed it, so the synced
|
|
484
493
|
// document is ALREADY there and the changes must not be applied a
|
|
485
494
|
// second time. Nothing is minted and no rebuild fires. The
|
|
@@ -491,19 +500,23 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
491
500
|
this.logUri(uri, `Skip rebuild: echo of a server-authored push (client version ${td.version})`, 'debug');
|
|
492
501
|
return;
|
|
493
502
|
}
|
|
503
|
+
if (origin?.kind === 'unreconstructable') {
|
|
504
|
+
this.tracer.with(uri).warn(`Drop change: no known client buffer for its ranges (client version ${td.version})`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
494
507
|
|
|
495
508
|
// The SHARED version advances iff the content actually changes — the
|
|
496
|
-
// invariant optimistic
|
|
509
|
+
// invariant optimistic based-on gates rely on. The new text is
|
|
497
510
|
// only known after applying the (possibly incremental) changes, so
|
|
498
511
|
// apply at a tentative +1 and roll the version back on an identical
|
|
499
512
|
// result (an empty-changes update only re-stamps the version).
|
|
500
513
|
//
|
|
501
|
-
// A divergent
|
|
502
|
-
//
|
|
503
|
-
//
|
|
514
|
+
// A divergent change is applied as its RECONSTRUCTED text rather than
|
|
515
|
+
// as its own ranges: those ranges address the client's own buffer, so
|
|
516
|
+
// applying them here would splice the wrong lines.
|
|
504
517
|
const previousText = document.getText();
|
|
505
518
|
const sharedVersion = document.version;
|
|
506
|
-
document = this.configuration.update(document,
|
|
519
|
+
document = this.configuration.update(document, origin === undefined ? changes : [{ text: origin.text }], sharedVersion + 1);
|
|
507
520
|
const changed = document.getText() !== previousText;
|
|
508
521
|
if (!changed) {
|
|
509
522
|
document = this.configuration.update(document, [], sharedVersion);
|
|
@@ -544,7 +557,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
544
557
|
* A content-identical write still fires the change event (rebuild): the
|
|
545
558
|
* authored write's server-side rebuild is correctness-bearing, it just
|
|
546
559
|
* mints no new version — nothing observable changed, so watchers'
|
|
547
|
-
*
|
|
560
|
+
* based-on versions stay valid.
|
|
548
561
|
*
|
|
549
562
|
* Returns the resulting shared version. Throws when the document is not
|
|
550
563
|
* open — callers (`AstDocumentManager.update`) open first.
|
|
@@ -644,18 +657,20 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
644
657
|
if (this.isOpenInClient(uri, clientId)) {
|
|
645
658
|
// Already open for this client under this canonical identity. If this is a
|
|
646
659
|
// 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
|
-
//
|
|
660
|
+
// divergent path, e.g. a symlink and its real path), record it so outbound
|
|
661
|
+
// edits reach this tab too — but do NOT re-fire open/rebuild; the document
|
|
662
|
+
// is already live.
|
|
650
663
|
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
651
664
|
const clientFacing = this.toLanguageClientUri(td.uri);
|
|
652
665
|
const record = this.__documents.get(uri);
|
|
653
666
|
if (record && !record.languageClientUris?.has(clientFacing)) {
|
|
654
667
|
(record.languageClientUris ??= new Set<LanguageClientUri>()).add(clientFacing);
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
668
|
+
// This tab's own buffer, NOT the synced text: a second tab is a second
|
|
669
|
+
// client model, read from disk, so a server-authored write already
|
|
670
|
+
// applied to the first tab leaves it BEHIND the synced document. Keying
|
|
671
|
+
// a diff to the synced text here addresses lines this tab does not have
|
|
672
|
+
// and drops content it does.
|
|
673
|
+
this.__shadow.setOpenedText(clientFacing, td.text);
|
|
659
674
|
}
|
|
660
675
|
}
|
|
661
676
|
return;
|
|
@@ -679,7 +694,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
679
694
|
const source = pendingText ? ', source=pending' : '';
|
|
680
695
|
// The SHARED version is server-assigned: continue the persisted
|
|
681
696
|
// sequence — same version when the content is unchanged since the
|
|
682
|
-
// last close (so watchers'
|
|
697
|
+
// last close (so watchers' based-on versions stay valid), one
|
|
683
698
|
// step when it changed (so no stale pointer can coincidentally pass
|
|
684
699
|
// the optimistic gate). Only a first-ever open adopts the client's
|
|
685
700
|
// declared id as the sequence seed.
|
|
@@ -694,22 +709,77 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
694
709
|
// Baseline the shadow to what Monaco just opened so the next outbound
|
|
695
710
|
// applyEditToLanguageClient diffs against the right starting point. Keyed by the
|
|
696
711
|
// language-client URI (what Monaco holds), not the canonical document key.
|
|
697
|
-
|
|
712
|
+
//
|
|
713
|
+
// The CLIENT's declared text, never the synced document: staged content
|
|
714
|
+
// consumed above leaves the two different, and a baseline asserting the
|
|
715
|
+
// client already holds the staged text suppresses the one sync that would
|
|
716
|
+
// deliver it. Skipped entirely when a push is already outstanding for this
|
|
717
|
+
// URI — an `applyEdit` to a closed file has the client open from disk and
|
|
718
|
+
// apply afterwards, so that shadow records what it is about to hold and
|
|
719
|
+
// disk text would key the next diff to a buffer nobody has.
|
|
720
|
+
const clientFacing = this.toLanguageClientUri(td.uri);
|
|
721
|
+
if (!this.__shadow.isTracked(clientFacing)) {
|
|
722
|
+
this.__shadow.set(clientFacing, td.text);
|
|
723
|
+
}
|
|
698
724
|
}
|
|
699
725
|
const toFire = Object.freeze({ document, clientId });
|
|
700
726
|
this.__onDidOpen.fire(toFire);
|
|
701
727
|
this.__onDidChangeContent.fire(toFire);
|
|
702
728
|
} else {
|
|
703
729
|
// An additional client attaches to a document already open by another client.
|
|
704
|
-
this.
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
730
|
+
this.logClientJoined(uri, clientId, document.version, existingClients);
|
|
731
|
+
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
732
|
+
// Monaco's own buffer, which on this path is NOT the synced text — another
|
|
733
|
+
// client opened the document and may already have changed it. Recorded as
|
|
734
|
+
// an equality-only baseline (never a diff one, see `setOpenedText`) so the
|
|
735
|
+
// refresh below does not push a full replace of content Monaco already
|
|
736
|
+
// holds, which would dirty the file on open.
|
|
737
|
+
this.__shadow.setOpenedText(this.toLanguageClientUri(td.uri), td.text);
|
|
738
|
+
}
|
|
709
739
|
this.refreshContent(uri, clientId);
|
|
710
740
|
}
|
|
711
741
|
}
|
|
712
742
|
|
|
743
|
+
/**
|
|
744
|
+
* Record `clientId` as an additional holder of a document another client
|
|
745
|
+
* already has open.
|
|
746
|
+
*
|
|
747
|
+
* Unlike the attach branch of {@link notifyDidOpenTextDocument} this does
|
|
748
|
+
* not refresh. That branch re-renders an attaching TEXTUAL view; a client
|
|
749
|
+
* reaching a document through a non-textual route has nothing to re-render,
|
|
750
|
+
* and refreshing per attach turns each into a Langium rebuild and dependent
|
|
751
|
+
* relink cascade.
|
|
752
|
+
*
|
|
753
|
+
* The staleness guard baselines at the SYNCED version, not a declared buffer
|
|
754
|
+
* version, because a client arriving this way holds no buffer of its own.
|
|
755
|
+
* Unifying the two routes therefore rebaselines a textual client's guard to a
|
|
756
|
+
* version it never declared.
|
|
757
|
+
*
|
|
758
|
+
* Returns whether a hold was added — `false` when `uri` is not open, or
|
|
759
|
+
* `clientId` already holds it.
|
|
760
|
+
*/
|
|
761
|
+
attachClient(uri: DocumentUri, clientId: string): boolean {
|
|
762
|
+
const key = this.documentKey(uri);
|
|
763
|
+
const document = this.__syncedDocuments.get(key);
|
|
764
|
+
if (!document || this.isOpenInClient(key, clientId)) {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
const record = this.trackingFor(key);
|
|
768
|
+
const existingClients = [...record.clients];
|
|
769
|
+
record.clients.add(clientId);
|
|
770
|
+
record.clientVersions.set(clientId, document.version);
|
|
771
|
+
this.logClientJoined(key, clientId, document.version, existingClients);
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
protected logClientJoined(uri: DocumentUri, clientId: string, version: number, existingClients: readonly string[]): void {
|
|
776
|
+
this.log(
|
|
777
|
+
uri,
|
|
778
|
+
`Attach client: ${clientId} joined existing document (version ${version}, ` +
|
|
779
|
+
`now open in: ${[...existingClients, clientId].join(', ')})`
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
713
783
|
refreshContent(uri: DocumentUri, clientId: string): void {
|
|
714
784
|
const syncedDocument = this.__syncedDocuments.get(this.documentKey(uri));
|
|
715
785
|
if (syncedDocument) {
|
|
@@ -790,7 +860,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
790
860
|
* `0` for a URI this store has never seen. The shared sequence is
|
|
791
861
|
* server-owned and monotonic across close/reopen cycles, and advances
|
|
792
862
|
* exactly when the synced content changes — which is what makes it a sound
|
|
793
|
-
* optimistic-concurrency token (
|
|
863
|
+
* optimistic-concurrency token (based-on gates): version unchanged ⇔
|
|
794
864
|
* content unchanged.
|
|
795
865
|
*/
|
|
796
866
|
version(uri: DocumentUri): number {
|
|
@@ -967,10 +1037,11 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
967
1037
|
const targets: Iterable<LanguageClientUri> = recorded && recorded.size > 0 ? recorded : [this.toLanguageClientUri(uri)];
|
|
968
1038
|
let lastResult: ApplyWorkspaceEditResult | undefined;
|
|
969
1039
|
for (const targetUri of targets) {
|
|
970
|
-
// Read BEFORE computeEdits, which overwrites
|
|
971
|
-
//
|
|
972
|
-
// echo of
|
|
973
|
-
|
|
1040
|
+
// Read BEFORE computeEdits, which overwrites the baseline and drops
|
|
1041
|
+
// the opened snapshot. This is the text the client holds, and
|
|
1042
|
+
// therefore the only text its echo of this push can be reconstructed
|
|
1043
|
+
// against — whether or not the push below is keyed to it.
|
|
1044
|
+
const before = this.__shadow.clientText(targetUri);
|
|
974
1045
|
const edits = this.__shadow.computeEdits(targetUri, newText);
|
|
975
1046
|
if (edits.length === 0) {
|
|
976
1047
|
continue;
|
|
@@ -985,9 +1056,20 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
985
1056
|
// it is exactly what the caller retries with after a rejection, so
|
|
986
1057
|
// gating it there would refuse the recovery too.
|
|
987
1058
|
const version = isFullReplace(edits) ? UNKNOWN_CLIENT_VERSION : this.languageClientVersion(uri);
|
|
1059
|
+
// A full `ApplyWorkspaceEditParams`, `edit` and all — NOT a bare
|
|
1060
|
+
// `WorkspaceEdit` with a `label` beside it. `applyEdit` takes
|
|
1061
|
+
// `ApplyWorkspaceEditParams | WorkspaceEdit` and discriminates on
|
|
1062
|
+
// `!!value.edit`, so `{ label, documentChanges }` is wrapped as
|
|
1063
|
+
// `{ edit: { label, documentChanges } }` — putting the label inside
|
|
1064
|
+
// the edit, where LSP defines no such field and no client reads it.
|
|
1065
|
+
// The union is also what hides it at compile time: excess-property
|
|
1066
|
+
// checking admits a property present in EITHER member, so an object
|
|
1067
|
+
// matching neither type-checks against the union.
|
|
988
1068
|
const result = await connection.workspace.applyEdit({
|
|
989
1069
|
label: options?.label,
|
|
990
|
-
|
|
1070
|
+
edit: {
|
|
1071
|
+
documentChanges: [TextDocumentEdit.create(OptionalVersionedTextDocumentIdentifier.create(targetUri, version), edits)]
|
|
1072
|
+
}
|
|
991
1073
|
});
|
|
992
1074
|
if (result && result.applied === false) {
|
|
993
1075
|
this.__shadow.invalidate(targetUri);
|
|
@@ -1043,12 +1125,17 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
1043
1125
|
|
|
1044
1126
|
/**
|
|
1045
1127
|
* Decide what an incoming language-client change actually is, by
|
|
1046
|
-
* reconstructing the client's resulting buffer against the
|
|
1047
|
-
* in flight for
|
|
1128
|
+
* reconstructing the client's resulting buffer against the text its ranges
|
|
1129
|
+
* address — the pre-push buffer of the OLDEST push still in flight for
|
|
1130
|
+
* `clientFacing`, else whatever that client is believed to hold.
|
|
1048
1131
|
*
|
|
1049
|
-
* `undefined` when
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1132
|
+
* `undefined` when the client's ranges address the synced text itself —
|
|
1133
|
+
* nothing in flight, and no evidence the client holds anything else. That
|
|
1134
|
+
* is the ordinary path, and the caller then applies the ranges directly.
|
|
1135
|
+
* The two texts part company without a push in flight whenever a client
|
|
1136
|
+
* attaches to a document another client has already written: it opened
|
|
1137
|
+
* from disk, so applying its ranges to the synced text splices lines they
|
|
1138
|
+
* never addressed.
|
|
1052
1139
|
*
|
|
1053
1140
|
* **Why the oldest, and why reconstruct at all.** The client applies our
|
|
1054
1141
|
* pushes in order and echoes each against the buffer it held before that
|
|
@@ -1079,22 +1166,41 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
1079
1166
|
document: T,
|
|
1080
1167
|
changes: TextDocumentContentChangeEvent[]
|
|
1081
1168
|
): LanguageClientChangeOrigin | undefined {
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1169
|
+
const queued = this.__pendingPushes.get(clientFacing);
|
|
1170
|
+
const pending = queued?.length ? queued : undefined;
|
|
1171
|
+
// The text the client's ranges address: the buffer it held before the
|
|
1172
|
+
// oldest push still in flight, else the buffer it is believed to hold.
|
|
1173
|
+
const clientText = pending?.[0].before ?? this.__shadow.clientText(clientFacing);
|
|
1174
|
+
if (pending === undefined && (clientText === undefined || clientText === document.getText())) {
|
|
1084
1175
|
return undefined;
|
|
1085
1176
|
}
|
|
1177
|
+
if (pending !== undefined && pending[0].before === undefined && changes.some(change => ContentChange.isIncremental(change))) {
|
|
1178
|
+
// A push sent to a client whose buffer was unknown — a first sync, or
|
|
1179
|
+
// the recovery push after a rejection. The shadow now holds the text
|
|
1180
|
+
// that push MOVES the client to, which is the one text the echo's
|
|
1181
|
+
// ranges provably do not address, so the fallback above is a baseline
|
|
1182
|
+
// known to be wrong rather than merely unverified. Drop the queue and
|
|
1183
|
+
// the shadow: the next sync is then a full replace, which lands on
|
|
1184
|
+
// whatever the client holds. A full-text change is exempt because it
|
|
1185
|
+
// reconstructs identically against any baseline.
|
|
1186
|
+
pending.length = 0;
|
|
1187
|
+
this.__shadow.invalidate(clientFacing);
|
|
1188
|
+
return { kind: 'unreconstructable' };
|
|
1189
|
+
}
|
|
1086
1190
|
// Through the configured factories, not `TextDocument` directly, so an
|
|
1087
1191
|
// adopter's custom text-document type governs how the ranges are applied
|
|
1088
1192
|
// here exactly as it does on the synced document.
|
|
1089
|
-
const probe = this.create(clientFacing, document.languageId, 0,
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1193
|
+
const probe = this.create(clientFacing, document.languageId, 0, clientText ?? document.getText());
|
|
1194
|
+
const reconstructed = this.update(probe, changes, 0).getText();
|
|
1195
|
+
if (pending !== undefined) {
|
|
1196
|
+
const matchIndex = pending.findIndex(push => push.afterHash === contentHash(reconstructed));
|
|
1197
|
+
if (matchIndex >= 0) {
|
|
1198
|
+
pending.splice(0, matchIndex + 1);
|
|
1199
|
+
return { kind: 'echo' };
|
|
1200
|
+
}
|
|
1201
|
+
pending.length = 0;
|
|
1095
1202
|
}
|
|
1096
|
-
|
|
1097
|
-
return { kind: 'divergent', text: clientText };
|
|
1203
|
+
return { kind: 'divergent', text: reconstructed };
|
|
1098
1204
|
}
|
|
1099
1205
|
|
|
1100
1206
|
/**
|
|
@@ -57,12 +57,23 @@ export function isFullReplace(edits: readonly TextEdit[]): boolean {
|
|
|
57
57
|
* - {@link computeEdits} after each outgoing server→client sync (updates the shadow optimistically).
|
|
58
58
|
* - {@link set} when the server receives a `didChange` from the language client (the client now
|
|
59
59
|
* holds that text; we shouldn't resend it).
|
|
60
|
+
* - {@link setOpenedText} when the client opens a document the server did not baseline for it —
|
|
61
|
+
* an equality-only baseline, since that text is a snapshot rather than a tracked one.
|
|
60
62
|
* - {@link invalidate} when the client rejects an edit (`applied=false`) or a doc is closed —
|
|
61
63
|
* the shadow is now stale and the next sync must fall back to a full replace.
|
|
62
64
|
*/
|
|
63
65
|
export class LanguageClientTextShadow {
|
|
64
66
|
protected shadow = new Map<string, string>();
|
|
65
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Per URI, the text the client declared it opened the document with, held
|
|
70
|
+
* only until a real baseline exists. Consulted by {@link computeEdits} when
|
|
71
|
+
* there is no shadow, and dropped on {@link invalidate}: a snapshot that
|
|
72
|
+
* outlived what the client holds would let the equality check skip an edit
|
|
73
|
+
* the client still needs.
|
|
74
|
+
*/
|
|
75
|
+
protected opened = new Map<string, string>();
|
|
76
|
+
|
|
66
77
|
/**
|
|
67
78
|
* @param onFallback invoked when {@link computeEdits} falls back to a full-range replace due to
|
|
68
79
|
* apply-verify mismatch. Useful for logging regressions.
|
|
@@ -85,23 +96,54 @@ export class LanguageClientTextShadow {
|
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
99
|
+
* Record the buffer the language client declared at `didOpen`, for a URI
|
|
100
|
+
* with no baseline yet.
|
|
101
|
+
*
|
|
102
|
+
* Deliberately NOT a {@link set}: this text is only ever compared for
|
|
103
|
+
* EQUALITY, never diffed against. A line-keyed diff is position-dependent,
|
|
104
|
+
* so keying one to a `didOpen` snapshot the client may have moved past
|
|
105
|
+
* splices its buffer — the failure {@link computeEdits}'s full-replace
|
|
106
|
+
* fallback exists to avoid.
|
|
107
|
+
*/
|
|
108
|
+
setOpenedText(uri: string, text: string): void {
|
|
109
|
+
this.opened.set(uri, text);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The text the language client is believed to hold: the tracked text, else
|
|
114
|
+
* the buffer it declared at `didOpen`.
|
|
90
115
|
*
|
|
91
116
|
* Read at the `workspace/applyEdit` egress BEFORE {@link computeEdits},
|
|
92
|
-
* which overwrites
|
|
93
|
-
*
|
|
94
|
-
* it
|
|
95
|
-
*
|
|
96
|
-
*
|
|
117
|
+
* which overwrites the tracked text and drops the opened snapshot. A change
|
|
118
|
+
* the client sends carries ranges addressing this text and nothing else, so
|
|
119
|
+
* losing it leaves that change unreconstructable — and applying its ranges
|
|
120
|
+
* to the already-advanced synced text splices lines they never addressed.
|
|
121
|
+
*
|
|
122
|
+
* Deliberately wider than what {@link computeEdits} may diff from: an
|
|
123
|
+
* opened snapshot is a position-dependent key to a buffer the client may
|
|
124
|
+
* have moved past, so keying a diff to it splices that buffer.
|
|
97
125
|
*/
|
|
98
|
-
|
|
99
|
-
return this.shadow.get(uri);
|
|
126
|
+
clientText(uri: string): string | undefined {
|
|
127
|
+
return this.shadow.get(uri) ?? this.opened.get(uri);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Whether a tracked text exists for `uri`, which is what decides whether a
|
|
132
|
+
* push can be diffed at all. Distinct from {@link clientText}, which also
|
|
133
|
+
* answers from the opened snapshot and so cannot settle this.
|
|
134
|
+
*/
|
|
135
|
+
isTracked(uri: string): boolean {
|
|
136
|
+
return this.shadow.has(uri);
|
|
100
137
|
}
|
|
101
138
|
|
|
102
139
|
/** Forget the shadow for a URI; next {@link computeEdits} falls back to a full-document replace. */
|
|
103
140
|
invalidate(uri: string): void {
|
|
104
141
|
this.shadow.delete(uri);
|
|
142
|
+
// Drops the opened snapshot too, and this is the only place that has to:
|
|
143
|
+
// `computeEdits` clears it whenever it consults it, so the sole way it can
|
|
144
|
+
// outlive what the client holds is surviving under a live shadow until that
|
|
145
|
+
// shadow is dropped here.
|
|
146
|
+
this.opened.delete(uri);
|
|
105
147
|
}
|
|
106
148
|
|
|
107
149
|
/**
|
|
@@ -109,7 +151,9 @@ export class LanguageClientTextShadow {
|
|
|
109
151
|
* the tracked shadow to `newText`. Updates the shadow to `newText` so subsequent calls diff
|
|
110
152
|
* against it.
|
|
111
153
|
*
|
|
112
|
-
* - Empty array when the shadow already equals `newText` (caller should skip the RPC)
|
|
154
|
+
* - Empty array when the shadow already equals `newText` (caller should skip the RPC),
|
|
155
|
+
* or when there is no shadow and the client opened the document with exactly `newText`
|
|
156
|
+
* (see {@link setOpenedText}) — pushing it anyway dirties the client's buffer on open.
|
|
113
157
|
* - Full-range replace when there is no shadow (first sync or post-{@link invalidate}).
|
|
114
158
|
* - Line-level diff otherwise. Before returning, we locally reconstruct `newText` by applying
|
|
115
159
|
* the diff to `old`; if that doesn't match, we fall back to a full-range replace and notify
|
|
@@ -124,7 +168,11 @@ export class LanguageClientTextShadow {
|
|
|
124
168
|
this.shadow.set(uri, newText);
|
|
125
169
|
const fullReplace: TextEdit = { range: FULL_RANGE, newText };
|
|
126
170
|
if (old === undefined) {
|
|
127
|
-
|
|
171
|
+
// The opened snapshot has served its purpose either way: the shadow above
|
|
172
|
+
// is now the baseline, so leaving it would only risk a later stale match.
|
|
173
|
+
const openedText = this.opened.get(uri);
|
|
174
|
+
this.opened.delete(uri);
|
|
175
|
+
return openedText === newText ? [] : [fullReplace];
|
|
128
176
|
}
|
|
129
177
|
const edits = diffToEdits(old, newText);
|
|
130
178
|
const verifyDoc = this.documents.create(uri, 'plaintext', 0, old);
|
|
@@ -22,10 +22,32 @@ export interface SelfSaveRegistryOptions {
|
|
|
22
22
|
/**
|
|
23
23
|
* Tracks `mtime` of files written by the server itself so the echo
|
|
24
24
|
* `didChangeWatchedFiles` event from the OS file watcher can be suppressed,
|
|
25
|
-
* avoiding redundant rebuilds after each save.
|
|
26
|
-
* (not by a time window) and expire after a TTL.
|
|
25
|
+
* avoiding redundant rebuilds after each save.
|
|
27
26
|
*/
|
|
28
|
-
export
|
|
27
|
+
export interface SelfSaveRegistry {
|
|
28
|
+
/**
|
|
29
|
+
* Record a self-write, and drop any registration already past its TTL.
|
|
30
|
+
*
|
|
31
|
+
* Keyed by `fsPath` rather than URI string, because the two sides of a
|
|
32
|
+
* suppression see different URI forms for one file.
|
|
33
|
+
*/
|
|
34
|
+
register(fsPath: string, mtimeMs: number): void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether `fsPath` carries a live registration at exactly `mtimeMs`.
|
|
38
|
+
*
|
|
39
|
+
* A registration expires after the TTL, so a write recorded longer ago than
|
|
40
|
+
* that answers `false` — the caller's decision holds only while the echo it
|
|
41
|
+
* suppresses could still be in flight. A query never mutates the registry.
|
|
42
|
+
*/
|
|
43
|
+
isRegistered(fsPath: string, mtimeMs: number): boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Compares entries by mtime rather than by a time window, so the echo of a
|
|
48
|
+
* save whose content is unchanged is still recognised.
|
|
49
|
+
*/
|
|
50
|
+
export class DefaultSelfSaveRegistry implements SelfSaveRegistry {
|
|
29
51
|
protected readonly entries = new Map<string, { mtimeMs: number; registeredAt: number }>();
|
|
30
52
|
/**
|
|
31
53
|
* Read time through the shared {@link Clock} rather than `Date.now()`: the
|
|
@@ -59,23 +81,40 @@ export class SelfSaveRegistry {
|
|
|
59
81
|
return isCaseInsensitiveFileSystem() ? fsPath.toLowerCase() : fsPath;
|
|
60
82
|
}
|
|
61
83
|
|
|
62
|
-
/** Record a self-write. Key by `fsPath` (not URI string) to avoid URI-form mismatches. */
|
|
63
84
|
register(fsPath: string, mtimeMs: number): void {
|
|
85
|
+
this.evictExpired();
|
|
64
86
|
this.entries.set(this.key(fsPath), { mtimeMs, registeredAt: this.clock.now() });
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const entry = this.entries.get(key);
|
|
71
|
-
if (!entry) {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
if (this.clock.now() - entry.registeredAt > this.ttlMs) {
|
|
75
|
-
this.entries.delete(key);
|
|
89
|
+
isRegistered(fsPath: string, mtimeMs: number): boolean {
|
|
90
|
+
const entry = this.entries.get(this.key(fsPath));
|
|
91
|
+
if (!entry || this.isExpired(entry.registeredAt)) {
|
|
76
92
|
return false;
|
|
77
93
|
}
|
|
78
94
|
// Accept mtime equality (some filesystems have 1-second precision).
|
|
79
95
|
return entry.mtimeMs === mtimeMs;
|
|
80
96
|
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Drop every expired registration.
|
|
100
|
+
*
|
|
101
|
+
* On the WRITE path, so the query stays free of side effects. Evicting from
|
|
102
|
+
* the query instead reaches only the keys something happens to ask about,
|
|
103
|
+
* and a path written once and never queried again is then retained for the
|
|
104
|
+
* life of the process. Bounded by the paths self-written inside one TTL
|
|
105
|
+
* window, so the sweep is over a handful of entries and sits behind a file
|
|
106
|
+
* write either way.
|
|
107
|
+
*/
|
|
108
|
+
protected evictExpired(): void {
|
|
109
|
+
for (const [key, entry] of this.entries) {
|
|
110
|
+
if (this.isExpired(entry.registeredAt)) {
|
|
111
|
+
this.entries.delete(key);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Strictly greater, so an entry aged exactly to the TTL is still eligible. */
|
|
117
|
+
protected isExpired(registeredAt: number): boolean {
|
|
118
|
+
return this.clock.now() - registeredAt > this.ttlMs;
|
|
119
|
+
}
|
|
81
120
|
}
|
package/src/index.ts
CHANGED
|
@@ -35,5 +35,10 @@ export * from './langium/workspace/index.js';
|
|
|
35
35
|
export * from './langium/service-registry.js';
|
|
36
36
|
export * from './langium/language-types.js';
|
|
37
37
|
export * from './launcher/index.js';
|
|
38
|
+
export * from './locale/index.js';
|
|
39
|
+
// The renderer only. The `./messages` subpath additionally enumerates this
|
|
40
|
+
// package's own declarations, which the root barrel already re-exports through
|
|
41
|
+
// the modules that raise them.
|
|
42
|
+
export * from './messages/renderer.js';
|
|
38
43
|
export * from './documents/index.js';
|
|
39
44
|
export * from './util/index.js';
|