@hydranium/core 1.0.0-next.5 → 1.0.0-next.50
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 +42 -5
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +9 -12
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +41 -9
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/language-client-text-shadow.d.ts +24 -1
- package/lib/documents/language-client-text-shadow.d.ts.map +1 -1
- package/lib/documents/language-client-text-shadow.js +36 -2
- 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/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 +103 -7
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +70 -89
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +82 -13
- package/lib/langium/module.d.ts.map +1 -1
- package/lib/langium/module.js +21 -12
- package/lib/langium/module.js.map +1 -1
- package/lib/langium/naming/name-provider.d.ts +10 -0
- package/lib/langium/naming/name-provider.d.ts.map +1 -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 +33 -2
- package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
- package/lib/langium/transfer/transfer-encoder.js +14 -3
- package/lib/langium/transfer/transfer-encoder.js.map +1 -1
- package/lib/langium/validation/document-validator.d.ts +145 -6
- package/lib/langium/validation/document-validator.d.ts.map +1 -1
- package/lib/langium/validation/document-validator.js +211 -4
- 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/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/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.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 +14 -3
- 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 +31 -4
- 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/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 +3 -4
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +2 -5
- 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-model-service.d.ts +1 -1
- 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 +55 -14
- package/src/documents/hydranium-text-documents.ts +41 -9
- package/src/documents/language-client-text-shadow.ts +38 -2
- 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/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 +145 -22
- package/src/langium/module.ts +99 -19
- package/src/langium/naming/name-provider.ts +10 -0
- 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 +45 -3
- package/src/langium/validation/document-validator.ts +255 -8
- 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/messages/carriers.ts +78 -0
- package/src/messages/index.ts +27 -0
- package/src/messages/renderer.ts +204 -0
- package/src/node/node-file-system-provider.ts +1 -41
- package/src/node/profiling-run.ts +1 -1
- package/src/node/rename-over-open-readers.ts +78 -0
- package/src/testing/fake-document.ts +22 -4
- package/src/testing/make-noop-shared-services.ts +50 -2
- package/src/testing/make-test-services.ts +49 -7
- package/src/testing/node/spawned-server.ts +35 -1
- 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 +5 -10
- package/src/testing/stub-document-builder.ts +32 -3
- package/src/testing/stub-model-service.ts +2 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Containment-array maintenance for callers that build or delete AST nodes
|
|
12
|
+
* programmatically — a GLSP operation handler being the usual one.
|
|
13
|
+
*
|
|
14
|
+
* **These live beside the key provider because the constraint is the key
|
|
15
|
+
* provider's.** `NameBasedKeyProvider` keys a node the `NameProvider` gives no
|
|
16
|
+
* name from its position, as `` `${$containerProperty}@${$containerIndex}` ``,
|
|
17
|
+
* so a node appended with a bare `array.push` carries neither and
|
|
18
|
+
* `getElementKey` answers `undefined` — the element has no key at all, and the
|
|
19
|
+
* GLSP index cannot address it. Deleting from the middle of such a list must
|
|
20
|
+
* renumber the survivors, or an id derived later in the same command names the
|
|
21
|
+
* wrong node. Shipping the constraint's statement without the code that
|
|
22
|
+
* discharges it leaves every adopter in that position re-deriving thirty lines
|
|
23
|
+
* from prose, and both failures are silent.
|
|
24
|
+
*
|
|
25
|
+
* The population is narrower than "anyone creating elements from a diagram": it
|
|
26
|
+
* takes a grammar with UNNAMED element types, programmatic creation or deletion
|
|
27
|
+
* of them, and this key provider. A grammar whose diagram elements are all
|
|
28
|
+
* named never reaches it, and the two ways out are named on the provider —
|
|
29
|
+
* give the type a name, or bind a provider whose fallback is content-derived.
|
|
30
|
+
*
|
|
31
|
+
* **Free functions, and deliberately not a step on the AST builder.** Building a
|
|
32
|
+
* node detached and appending it later is legitimate, so a containment step
|
|
33
|
+
* cannot be mandatory; and an optional fluent step could not check the index
|
|
34
|
+
* against the array unless it were handed the array, at which point it is these
|
|
35
|
+
* signatures with more ceremony. `AstNodeInit` already accepts the three fields
|
|
36
|
+
* for a caller who has them, which is the case these cover: it is the INDEX that
|
|
37
|
+
* cannot be known without reading the array.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import type { AstNode } from '@hydranium/langium';
|
|
41
|
+
import type { Mutable } from '@hydranium/protocol';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Append `child` to `children`, stamping the Langium containment plumbing
|
|
45
|
+
* (`$container`, `$containerProperty`, `$containerIndex`) so a positional key
|
|
46
|
+
* can address it.
|
|
47
|
+
*
|
|
48
|
+
* `property` and `children` are separate parameters rather than one property
|
|
49
|
+
* name the function dereferences: the array is what the index is derived from,
|
|
50
|
+
* so taking it directly is what makes the two agree by construction instead of
|
|
51
|
+
* by a lookup that a typo could point elsewhere.
|
|
52
|
+
*
|
|
53
|
+
* @returns `child`, so a caller can append and use it in one expression.
|
|
54
|
+
*/
|
|
55
|
+
export function appendChild<TChild extends AstNode>(container: AstNode, property: string, children: TChild[], child: TChild): TChild {
|
|
56
|
+
const mutable = child as Mutable<TChild>;
|
|
57
|
+
mutable.$container = container;
|
|
58
|
+
mutable.$containerProperty = property;
|
|
59
|
+
mutable.$containerIndex = children.length;
|
|
60
|
+
children.push(child);
|
|
61
|
+
return child;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Remove every entry of `children` that is in `toRemove`, then renumber the
|
|
66
|
+
* survivors' `$containerIndex`.
|
|
67
|
+
*
|
|
68
|
+
* Mutates `children` in place rather than returning a new array, because the
|
|
69
|
+
* caller's array IS the AST's containment list — replacing the reference would
|
|
70
|
+
* leave the parent pointing at the old one.
|
|
71
|
+
*
|
|
72
|
+
* @returns how many entries were removed, so a caller can tell a no-op delete
|
|
73
|
+
* from one that changed the model without comparing lengths itself.
|
|
74
|
+
*/
|
|
75
|
+
export function removeChildren<TChild extends AstNode>(children: TChild[], toRemove: ReadonlySet<TChild>): number {
|
|
76
|
+
if (toRemove.size === 0) {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
const survivors = children.filter(child => !toRemove.has(child));
|
|
80
|
+
const removed = children.length - survivors.length;
|
|
81
|
+
children.length = 0;
|
|
82
|
+
for (const [index, child] of survivors.entries()) {
|
|
83
|
+
(child as Mutable<TChild>).$containerIndex = index;
|
|
84
|
+
children.push(child);
|
|
85
|
+
}
|
|
86
|
+
return removed;
|
|
87
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
+
export * from './containment.js';
|
|
10
11
|
export * from './element-key-provider.js';
|
|
11
12
|
export * from './name-based-key-provider.js';
|
|
12
13
|
export * from './positional-key-provider.js';
|
|
@@ -55,6 +55,10 @@ import { type NameProvider } from '../naming/name-provider.js';
|
|
|
55
55
|
* - After a removal, surviving siblings need their `$containerIndex`
|
|
56
56
|
* renumbered, or a key derived afterwards addresses the wrong node.
|
|
57
57
|
*
|
|
58
|
+
* {@link appendChild} and {@link removeChildren} discharge both, and ship
|
|
59
|
+
* beside this provider because the constraint is this provider's rather than
|
|
60
|
+
* any caller's.
|
|
61
|
+
*
|
|
58
62
|
* Adopters that need insert / delete stability for unnamed types should
|
|
59
63
|
* give those types a name in the grammar, or bind a key provider whose
|
|
60
64
|
* fallback is content-derived rather than positional.
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type CanonicalUri,
|
|
12
12
|
type CloseModelArgs,
|
|
13
13
|
ConflictError,
|
|
14
|
+
defineMessage,
|
|
14
15
|
Logger,
|
|
15
16
|
type MaybeObservableValue,
|
|
16
17
|
type MaybePromise,
|
|
@@ -34,6 +35,23 @@ import { labelPhaseListener } from '../document-builder/labeled-phase-listener.j
|
|
|
34
35
|
import { LANGUAGE_CLIENT_ID } from '../../documents/client-ids.js';
|
|
35
36
|
import { type ServerSharedServices } from '../module.js';
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The undo-stack entry for a server-authored write pushed to the editor.
|
|
40
|
+
*
|
|
41
|
+
* **A user-facing LABEL, not a log string**, which is easy to miss because it
|
|
42
|
+
* travels as an options field rather than as a message: LSP specifies
|
|
43
|
+
* `ApplyWorkspaceEditParams.label` as "presented in the user interface for
|
|
44
|
+
* example on an undo stack to undo the workspace edit". So a user who edits
|
|
45
|
+
* through a form or drags a diagram node reads this in their editor's undo menu
|
|
46
|
+
* — which is why it is rendered like any other message the server sends rather
|
|
47
|
+
* than left as the English literal it was.
|
|
48
|
+
*
|
|
49
|
+
* Parameterless deliberately. The obvious improvement is to name the document,
|
|
50
|
+
* and it is the wrong one: an undo menu is already grouped under the file, so
|
|
51
|
+
* the URI would be noise in the one place it is redundant.
|
|
52
|
+
*/
|
|
53
|
+
export const MODEL_UPDATE_EDIT = defineMessage('hydranium/core/model-update-edit', 'Update Model');
|
|
54
|
+
|
|
37
55
|
/** Max time {@link ModelService.settleSave} waits for the build to settle and the sync chain to drain. */
|
|
38
56
|
const SAVE_SETTLE_TIMEOUT_MS = 10_000;
|
|
39
57
|
|
|
@@ -182,7 +200,74 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
182
200
|
* args. Constrained to {@link TransferElement}. Defaults to the
|
|
183
201
|
* structural base.
|
|
184
202
|
*/
|
|
185
|
-
|
|
203
|
+
/**
|
|
204
|
+
* The seam every non-LSP head talks to: the data server, the GLSP head and an
|
|
205
|
+
* adopter's own services reach documents through this slot rather than through
|
|
206
|
+
* the workspace stores.
|
|
207
|
+
*
|
|
208
|
+
* **Two families, and the distinction matters more than the names suggest.**
|
|
209
|
+
* `waitFor*` is a pure wait — it never triggers a build, so a caller waiting on
|
|
210
|
+
* a document no build has touched waits until something else builds it.
|
|
211
|
+
* `ensureDocumentState` and the phase shorthands over it *dispatch*: warm
|
|
212
|
+
* documents are awaited, cold ones are built.
|
|
213
|
+
*
|
|
214
|
+
* **Diagnostics are typed `never` below `Validated`.** Validation is the last
|
|
215
|
+
* phase, so at any earlier landmark the array either is not yet computed or
|
|
216
|
+
* still holds the previous build's, and reading it would take stale results
|
|
217
|
+
* for fresh ones. A caller that needs diagnostics asks for {@link validated}.
|
|
218
|
+
*
|
|
219
|
+
* What the `never` buys, exactly: reading a field off an element is a compile
|
|
220
|
+
* error, and nothing can be appended. It does NOT stop a caller assigning an
|
|
221
|
+
* element to a typed variable, because `never` is assignable to everything — so
|
|
222
|
+
* this is a guard against reaching for diagnostics by accident, not a seal
|
|
223
|
+
* against doing it deliberately.
|
|
224
|
+
*
|
|
225
|
+
* The waits resolve at or ABOVE their target, so an already-validated document
|
|
226
|
+
* does carry usable diagnostics and the `never` over-forbids there. That
|
|
227
|
+
* direction is the safe one: the alternative permits stale reads silently. A
|
|
228
|
+
* member taking a phase as a PARAMETER cannot judge statically and so returns
|
|
229
|
+
* `TDiagnostic`, leaving the choice to the caller.
|
|
230
|
+
*/
|
|
231
|
+
export interface ModelService<TAst extends AstNode, TDiagnostic = TransferDiagnostic, TTransfer extends TransferElement = TransferElement> {
|
|
232
|
+
/**
|
|
233
|
+
* Resolves once the workspace has been initialised and its first build has
|
|
234
|
+
* completed — the gate every read should wait behind, since a document
|
|
235
|
+
* queried before it may be unbuilt and reach no phase.
|
|
236
|
+
*
|
|
237
|
+
* A property rather than a method, matching `ProjectManager.ready` and
|
|
238
|
+
* Langium's `WorkspaceManager.ready`. An implementation needing a stricter
|
|
239
|
+
* gate supplies a Promise that awaits its own concern as well.
|
|
240
|
+
*/
|
|
241
|
+
readonly ready: Promise<void>;
|
|
242
|
+
|
|
243
|
+
// Pure waits — never trigger a build.
|
|
244
|
+
waitForDocumentState(uri: string, state: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
245
|
+
waitForDocumentSettled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
|
|
246
|
+
waitForBuilderState(state: DocumentState, cancelToken?: CancellationToken): Promise<void>;
|
|
247
|
+
|
|
248
|
+
// Wait if warm, build if cold.
|
|
249
|
+
ensureDocumentState(uri: string, state?: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
250
|
+
rebuild(uri: string, state?: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
251
|
+
parsed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
|
|
252
|
+
linked(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
|
|
253
|
+
settled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
|
|
254
|
+
indexed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
|
|
255
|
+
validated(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
256
|
+
|
|
257
|
+
update(args: TransferUpdateArgs<TTransfer>, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
258
|
+
save(args: TransferSaveArgs<TTransfer>, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
259
|
+
|
|
260
|
+
open(args: OpenModelArgs): Promise<Disposable>;
|
|
261
|
+
close(args: CloseModelArgs): Promise<void>;
|
|
262
|
+
isOpen(uri: string): boolean;
|
|
263
|
+
getDocument(uri: string): LangiumDocument | undefined;
|
|
264
|
+
|
|
265
|
+
onModelUpdated(uri: string, listener: (event: AstDocumentUpdatedEvent<TAst, TDiagnostic>) => void): Disposable;
|
|
266
|
+
onModelSaved(uri: string, listener: (event: AstDocumentSavedEvent<TAst, TDiagnostic>) => void): Disposable;
|
|
267
|
+
onClientClosed(uri: string, clientId: string, listener: () => void): Disposable;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export class DefaultModelService<
|
|
186
271
|
TAst extends AstNode,
|
|
187
272
|
TDiagnostic = TransferDiagnostic,
|
|
188
273
|
/**
|
|
@@ -195,7 +280,7 @@ export class ModelService<
|
|
|
195
280
|
* integrity service) that pass AST roots remain compatible.
|
|
196
281
|
*/
|
|
197
282
|
TTransfer extends TransferElement = TransferElement
|
|
198
|
-
> {
|
|
283
|
+
> implements ModelService<TAst, TDiagnostic, TTransfer> {
|
|
199
284
|
protected readonly tracer: Tracer;
|
|
200
285
|
/**
|
|
201
286
|
* The single document-identity seam (`services.workspace.DocumentUriPolicy`),
|
|
@@ -329,8 +414,12 @@ export class ModelService<
|
|
|
329
414
|
* {@link waitForDocumentState} for the common "wait until content is stable"
|
|
330
415
|
* case (e.g. settling a save). Pure wait — does not trigger a build.
|
|
331
416
|
*/
|
|
332
|
-
async waitForDocumentSettled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst,
|
|
333
|
-
return this.waitForDocumentStateCanonical(
|
|
417
|
+
async waitForDocumentSettled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
|
|
418
|
+
return (await this.waitForDocumentStateCanonical(
|
|
419
|
+
this.uriPolicy.canonicalUri(uri),
|
|
420
|
+
IntegrityService.SettledState,
|
|
421
|
+
cancelToken
|
|
422
|
+
)) as AstDocument<TAst, never>;
|
|
334
423
|
}
|
|
335
424
|
|
|
336
425
|
/**
|
|
@@ -484,11 +573,22 @@ export class ModelService<
|
|
|
484
573
|
*
|
|
485
574
|
* Phase invariants encoded in the return type:
|
|
486
575
|
* - `parsed` / `linked` / `settled` / `indexed` return
|
|
487
|
-
* `AstDocument<TAst, never>` — diagnostics
|
|
488
|
-
*
|
|
576
|
+
* `AstDocument<TAst, never>` — no diagnostics have been computed at
|
|
577
|
+
* those phases.
|
|
489
578
|
* - `validated` returns `AstDocument<TAst, TDiagnostic>` — diagnostics
|
|
490
579
|
* are populated.
|
|
491
580
|
*
|
|
581
|
+
* **The `never` is a claim about the PHASE, not a guarantee about the
|
|
582
|
+
* instance, and the gap is reachable rather than theoretical.** The wait
|
|
583
|
+
* underneath resolves at or ABOVE the requested state, so a document
|
|
584
|
+
* something else already carried past `Validated` comes back from
|
|
585
|
+
* `settled()` with a populated diagnostics array typed `never`. Nothing
|
|
586
|
+
* strips it — the envelope copies the live document's array verbatim — and
|
|
587
|
+
* any host that validates its workspace before a consumer asks produces
|
|
588
|
+
* exactly that. So read an empty array as "none were computed, or there are
|
|
589
|
+
* none", never as "this document is clean", and call {@link validated} when
|
|
590
|
+
* the answer has to mean the second.
|
|
591
|
+
*
|
|
492
592
|
* `settled` is the integrity-overlay name for "all integrity rules
|
|
493
593
|
* have fired"; it maps to {@link IntegrityService.SettledState} (which
|
|
494
594
|
* equals `DocumentState.IndexedReferences`), but the dedicated method
|
|
@@ -598,19 +698,29 @@ export class ModelService<
|
|
|
598
698
|
// created from the payload rather than read from the filesystem — `update`
|
|
599
699
|
// is an upsert. For an already-open document `open` refreshes content (the
|
|
600
700
|
// text is ignored on that branch), so existing-document behaviour is
|
|
601
|
-
// unchanged. `version` is intentionally NOT forwarded to `open
|
|
602
|
-
// create stays at its initial version
|
|
603
|
-
//
|
|
701
|
+
// unchanged. `version` is intentionally NOT forwarded to `open`, so a cold
|
|
702
|
+
// create stays at its initial version rather than adopting a number the
|
|
703
|
+
// caller chose.
|
|
704
|
+
//
|
|
705
|
+
// The gate's version is read BEFORE that open, and must be: for a document
|
|
706
|
+
// no client holds open, the open assigns the shared version from the
|
|
707
|
+
// INCOMING text, so a version read afterwards has already absorbed the
|
|
708
|
+
// caller's own write. Gating on it rejected every modifying write to a
|
|
709
|
+
// closed document, having compared the caller's `baseVersion` against a
|
|
710
|
+
// number the caller itself produced — and a serialised round-trip that is
|
|
711
|
+
// not byte-identical to the stored text was enough to trigger it. Reading
|
|
712
|
+
// first keeps both cases the gate exists for: an unknown URI answers 0, so
|
|
713
|
+
// a based-on-version update of a not-yet-existing document still trips it,
|
|
714
|
+
// and a genuine conflict still trips it, another writer having advanced the
|
|
715
|
+
// sequence past the version the caller read.
|
|
716
|
+
const currentVersion = this.services.workspace.TextDocuments.version(uri);
|
|
604
717
|
const text = await run('serialize', () => this.modelToText(uri, args.model, cancelToken));
|
|
605
718
|
await run('open', () => this.open({ uri, clientId: args.clientId, text }));
|
|
606
|
-
if (args.baseVersion !== undefined) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
this.tracer.debug(`Conflict on ${uri}: based-on v${args.baseVersion} stale, server at v${current}`);
|
|
612
|
-
throw new ConflictError(uri, args.baseVersion, current);
|
|
613
|
-
}
|
|
719
|
+
if (args.baseVersion !== undefined && currentVersion !== args.baseVersion) {
|
|
720
|
+
// Distinct from the post-build "superseded" debug line below: this is a
|
|
721
|
+
// based-on-stale rejection (the write never applies), not two writes racing.
|
|
722
|
+
this.tracer.debug(`Conflict on ${uri}: based-on v${args.baseVersion} stale, server at v${currentVersion}`);
|
|
723
|
+
throw new ConflictError(uri, args.baseVersion, currentVersion);
|
|
614
724
|
}
|
|
615
725
|
const appliedVersion = await run('apply', () => this.services.workspace.AstDocumentManager.update(uri, text, args.clientId));
|
|
616
726
|
// Dispatch through the public `rebuild` (which re-canonicalizes the already-
|
|
@@ -834,17 +944,17 @@ export class ModelService<
|
|
|
834
944
|
*
|
|
835
945
|
* The signal is "a known client other than the language client authored this
|
|
836
946
|
* version **and** the URI was in the last build's changed set
|
|
837
|
-
* (`
|
|
947
|
+
* (`isTriggeringEdit`)". A framework-internal rebuild reports no author
|
|
838
948
|
* (`getAuthor` → `undefined`), so it fails `hasKnownAuthor` without comparing
|
|
839
949
|
* against a sentinel. This is NOT redundant with content/registration — it
|
|
840
950
|
* distinguishes "client edited" from "framework rebuilt", which neither the
|
|
841
|
-
* shadow nor `
|
|
951
|
+
* shadow nor `isTriggeringEdit` alone can.
|
|
842
952
|
*/
|
|
843
953
|
protected isNonLanguageClientEdit(document: LangiumDocument): boolean {
|
|
844
954
|
const documents = this.services.workspace.AstDocumentManager;
|
|
845
955
|
const author = documents.getAuthor(document);
|
|
846
956
|
const hasKnownAuthor = !!author && author !== LANGUAGE_CLIENT_ID;
|
|
847
|
-
return hasKnownAuthor && documents.
|
|
957
|
+
return hasKnownAuthor && documents.isTriggeringEdit(document.textDocument.uri);
|
|
848
958
|
}
|
|
849
959
|
|
|
850
960
|
/**
|
|
@@ -878,13 +988,26 @@ export class ModelService<
|
|
|
878
988
|
this.syncChains.set(uri, chain);
|
|
879
989
|
}
|
|
880
990
|
|
|
991
|
+
/**
|
|
992
|
+
* The undo-stack label for a server-authored write, in the locale the server
|
|
993
|
+
* was handed at init.
|
|
994
|
+
*
|
|
995
|
+
* One method rather than the literal at each `applyEdit`, because the two
|
|
996
|
+
* call sites are the same edit — a push and its full-replace retry — and an
|
|
997
|
+
* undo menu showing two different words for one operation would read as two
|
|
998
|
+
* operations.
|
|
999
|
+
*/
|
|
1000
|
+
protected editLabel(): string {
|
|
1001
|
+
return this.services.MessageRenderer.renderMessage(MODEL_UPDATE_EDIT);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
881
1004
|
protected async drainSyncQueue(uri: string): Promise<void> {
|
|
882
1005
|
const uriLogger = this.tracer.withUri(uri);
|
|
883
1006
|
while (this.pendingSync.has(uri)) {
|
|
884
1007
|
const text = this.pendingSync.get(uri)!;
|
|
885
1008
|
this.pendingSync.delete(uri);
|
|
886
1009
|
try {
|
|
887
|
-
let result = await this.services.workspace.TextDocuments.applyEditToLanguageClient(uri, text, { label:
|
|
1010
|
+
let result = await this.services.workspace.TextDocuments.applyEditToLanguageClient(uri, text, { label: this.editLabel() });
|
|
888
1011
|
if (result?.applied === false && !this.pendingSync.has(uri)) {
|
|
889
1012
|
// The push is addressed at the client's LAST DECLARED VERSION, so a
|
|
890
1013
|
// rejection normally means the client's buffer moved while the
|
|
@@ -905,7 +1028,7 @@ export class ModelService<
|
|
|
905
1028
|
// queued — best-effort, since a settle arriving later simply pushes
|
|
906
1029
|
// after this and still wins.
|
|
907
1030
|
uriLogger.warn(`Language client rejected applyEdit at its declared version — re-pushing a full replace`);
|
|
908
|
-
result = await this.services.workspace.TextDocuments.applyEditToLanguageClient(uri, text, { label:
|
|
1031
|
+
result = await this.services.workspace.TextDocuments.applyEditToLanguageClient(uri, text, { label: this.editLabel() });
|
|
909
1032
|
if (result?.applied === false) {
|
|
910
1033
|
uriLogger.warn(`Language client rejected the full-replace retry too — client content is stale`);
|
|
911
1034
|
}
|
package/src/langium/module.ts
CHANGED
|
@@ -7,30 +7,33 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { type Clock, type Logger, type Project, type Tracer,
|
|
10
|
+
import { type Clock, type Logger, type Project, type Tracer, NoopLogger, SystemClock } from '@hydranium/protocol';
|
|
11
|
+
import { DefaultServerLocale, type ServerLocale } from '../locale/server-locale.js';
|
|
12
|
+
import { DefaultMessageRenderer, type MessageRenderer } from '../messages/renderer.js';
|
|
11
13
|
import { ServerTracer } from './diagnostics/server-tracer.js';
|
|
12
14
|
import { HydraniumLangiumProfiler } from './diagnostics/hydranium-langium-profiler.js';
|
|
13
15
|
import { type AstNode, type Module } from '@hydranium/langium';
|
|
14
16
|
import { type DefaultSharedModuleContext, type LangiumSharedServices, type PartialLangiumSharedServices } from '@hydranium/langium/lsp';
|
|
15
17
|
import { type TextDocument } from 'vscode-languageserver-textdocument';
|
|
16
|
-
import { ModelService } from './model-service/model-service.js';
|
|
18
|
+
import { DefaultModelService, type ModelService } from './model-service/model-service.js';
|
|
17
19
|
import { type ProjectManager } from './project/project-manager.js';
|
|
18
20
|
import { SingleProjectManager } from './project/single-project-manager.js';
|
|
19
|
-
import { TransferEncoder } from './transfer/transfer-encoder.js';
|
|
21
|
+
import { DefaultTransferEncoder, type TransferEncoder } from './transfer/transfer-encoder.js';
|
|
20
22
|
import { HydraniumDocumentBuilder } from './document-builder/document-builder.js';
|
|
21
|
-
import { BuildPipelineIntegration } from './document-builder/build-pipeline-integration.js';
|
|
23
|
+
import { DefaultBuildPipelineIntegration, type BuildPipelineIntegration } from './document-builder/build-pipeline-integration.js';
|
|
22
24
|
import { type BuildPhasePassContribution } from './build-phase-pass/build-phase-pass.js';
|
|
23
25
|
import { type BuildPhasePassService, DefaultBuildPhasePassService } from './build-phase-pass/build-phase-pass-service.js';
|
|
24
|
-
import { CstResidencyService } from './residency/index.js';
|
|
26
|
+
import { DefaultCstResidencyService, type CstResidencyService } from './residency/index.js';
|
|
25
27
|
import { HydraniumIndexManager } from './workspace/index-manager.js';
|
|
26
28
|
import { HydraniumWorkspaceManager } from './workspace/hydranium-workspace-manager.js';
|
|
27
29
|
import { HydraniumWorkspaceLock } from './workspace/hydranium-workspace-lock.js';
|
|
28
30
|
import { HydraniumLangiumDocumentFactory } from './workspace/hydranium-langium-document-factory.js';
|
|
31
|
+
import { type HydraniumDocumentRegistry, HydraniumLangiumDocuments } from './workspace/langium-documents.js';
|
|
29
32
|
import { type AdditionalDocumentContribution } from './workspace/additional-document-contribution.js';
|
|
30
33
|
import { DefaultDocumentUriPolicy, type DocumentUriPolicy } from './workspace/document-uri-policy.js';
|
|
31
34
|
import { HydraniumTextDocuments } from '../documents/hydranium-text-documents.js';
|
|
32
|
-
import { AstDocumentManager, type WritableFileSystemProvider } from '../documents/ast-document-manager.js';
|
|
33
|
-
import { SelfSaveRegistry } from '../documents/self-save-registry.js';
|
|
35
|
+
import { DefaultAstDocumentManager, type AstDocumentManager, type WritableFileSystemProvider } from '../documents/ast-document-manager.js';
|
|
36
|
+
import { DefaultSelfSaveRegistry, type SelfSaveRegistry } from '../documents/self-save-registry.js';
|
|
34
37
|
import { DefaultEmptyFileSystemProvider } from './workspace/file-system-provider.js';
|
|
35
38
|
import { type ServerLanguageServices } from './language-module.js';
|
|
36
39
|
import { ExtendedServiceRegistry } from './service-registry.js';
|
|
@@ -91,6 +94,24 @@ export interface ServerAddedSharedServices<TProject extends Project = Project> {
|
|
|
91
94
|
* plain `DefaultTracer` with their own `MemoryReader`.
|
|
92
95
|
*/
|
|
93
96
|
Tracer: Tracer;
|
|
97
|
+
/**
|
|
98
|
+
* Renders each user-facing message once, before the server sends it — so
|
|
99
|
+
* every message is rendered by the side that knows the reading user's
|
|
100
|
+
* language. All three heads inherit one diagnostics pass, which is what
|
|
101
|
+
* makes this the single slot an adopter with i18n rebinds; the same binding
|
|
102
|
+
* serves framework codes and their own `defineMessage` codes alike.
|
|
103
|
+
*
|
|
104
|
+
* The framework ships English only and selects no locale, so the default
|
|
105
|
+
* returns every sentence unchanged. Adopters subclass and override
|
|
106
|
+
* `translationsFor`.
|
|
107
|
+
*/
|
|
108
|
+
MessageRenderer: MessageRenderer;
|
|
109
|
+
/**
|
|
110
|
+
* The locale an init handed the server, for whoever renders in it. Held
|
|
111
|
+
* apart from the renderer so replacing the renderer cannot drop locale
|
|
112
|
+
* handling.
|
|
113
|
+
*/
|
|
114
|
+
ServerLocale: ServerLocale;
|
|
94
115
|
/**
|
|
95
116
|
* LSP-bound slots layered on top of Langium's `LangiumSharedLSPServices`.
|
|
96
117
|
* The framework contributes a single string slot here —
|
|
@@ -113,6 +134,12 @@ export interface ServerAddedSharedServices<TProject extends Project = Project> {
|
|
|
113
134
|
workspace: {
|
|
114
135
|
/* override */ TextDocuments: HydraniumTextDocuments<TextDocument>;
|
|
115
136
|
/* override */ WorkspaceManager: HydraniumWorkspaceManager;
|
|
137
|
+
/**
|
|
138
|
+
* Narrow Langium's registry slot to the framework surface, which adds
|
|
139
|
+
* `createEmptyDocument` — reachable only through this narrowing, and
|
|
140
|
+
* wanted by a scope provider querying a URI before the file exists.
|
|
141
|
+
*/
|
|
142
|
+
/* override */ LangiumDocuments: HydraniumDocumentRegistry;
|
|
116
143
|
/**
|
|
117
144
|
* Narrow Langium's `IndexManager` slot to the framework subclass, which
|
|
118
145
|
* layers an `elementsByName` map over Langium's index and exposes
|
|
@@ -228,14 +255,22 @@ export interface ServerAddedSharedServices<TProject extends Project = Project> {
|
|
|
228
255
|
* heads read both slots from shared services rather than constructor
|
|
229
256
|
* arguments.
|
|
230
257
|
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
258
|
+
* `TransferEncoder` is the INTERFACE, not `DefaultTransferEncoder` — an
|
|
259
|
+
* adopter can therefore REPLACE this declaration (see
|
|
260
|
+
* {@link WithServiceOverrides}) rather than intersect with it, which is what
|
|
261
|
+
* keeps slot resolution independent of the order a services type is written
|
|
262
|
+
* in. `ModelService` is still a class and does not yet have that property.
|
|
263
|
+
*
|
|
264
|
+
* A class in a slot costs two things, both measured. Its `protected` members
|
|
265
|
+
* join every assignability check and are compared NOMINALLY, so a subclass
|
|
266
|
+
* declared against a second physical copy of this package cannot satisfy it.
|
|
267
|
+
* And where the class is generic over a map reached through `keyof`, that
|
|
268
|
+
* parameter is measured INVARIANT — instantiations then relate only when
|
|
269
|
+
* their arguments are mutually assignable, which no adopter map is with the
|
|
270
|
+
* framework's. Neither survives on an interface.
|
|
236
271
|
*/
|
|
237
272
|
model: {
|
|
238
|
-
TransferEncoder: TransferEncoder
|
|
273
|
+
TransferEncoder: TransferEncoder;
|
|
239
274
|
ModelService: ModelService<AstNode, unknown>;
|
|
240
275
|
};
|
|
241
276
|
/**
|
|
@@ -279,6 +314,45 @@ export type ServerSharedServices<TProject extends Project = Project> = Omit<Lang
|
|
|
279
314
|
workspace: Omit<LangiumSharedServices['workspace'], 'TextDocuments' | 'DocumentBuilder'>;
|
|
280
315
|
} & ServerAddedSharedServices<TProject>;
|
|
281
316
|
|
|
317
|
+
/**
|
|
318
|
+
* The service-tree namespaces — the keys whose value groups further slots
|
|
319
|
+
* rather than being a slot itself.
|
|
320
|
+
*
|
|
321
|
+
* Consumed by {@link WithServiceOverrides} to know where to merge one level
|
|
322
|
+
* deeper. **Add a namespace to {@link ServerAddedSharedServices} and you must
|
|
323
|
+
* add it here**, and the failure of forgetting is silent in the direction that
|
|
324
|
+
* matters: an unlisted namespace is treated as a leaf, so an adopter overriding
|
|
325
|
+
* one slot inside it replaces the WHOLE namespace and loses the sibling slots
|
|
326
|
+
* with no diagnostic. The reverse mistake — listing a leaf — fails loudly.
|
|
327
|
+
*/
|
|
328
|
+
type ServiceNamespace = 'lsp' | 'workspace' | 'model';
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Compose an adopter's service tree so its declarations REPLACE the framework's
|
|
332
|
+
* rather than intersecting with them.
|
|
333
|
+
*
|
|
334
|
+
* Langium composes services by intersection, which accumulates: two
|
|
335
|
+
* declarations of one slot survive as an overload set, and which one a call
|
|
336
|
+
* resolves to depends on the order the intersection was written in — silently,
|
|
337
|
+
* with no diagnostic at the point a reorder changes it. There is no override
|
|
338
|
+
* operator for intersections, so the framework's declaration has to be removed
|
|
339
|
+
* before the adopter's is added. This does that, one level deep for each
|
|
340
|
+
* {@link ServiceNamespace}, so a narrowed slot replaces its framework twin
|
|
341
|
+
* while its siblings survive.
|
|
342
|
+
*
|
|
343
|
+
* Only slots typed as INTERFACES can be replaced this way. A class-typed slot
|
|
344
|
+
* drags its `protected` members into the assignability check — compared
|
|
345
|
+
* nominally — so an adopter subclass does not satisfy it and the framework's
|
|
346
|
+
* declaration cannot be dropped.
|
|
347
|
+
*/
|
|
348
|
+
export type WithServiceOverrides<TBase, TOverrides> = Omit<TBase, keyof TOverrides> & {
|
|
349
|
+
[K in keyof TOverrides]: K extends ServiceNamespace
|
|
350
|
+
? K extends keyof TBase
|
|
351
|
+
? Omit<TBase[K], keyof TOverrides[K]> & TOverrides[K]
|
|
352
|
+
: TOverrides[K]
|
|
353
|
+
: TOverrides[K];
|
|
354
|
+
};
|
|
355
|
+
|
|
282
356
|
/**
|
|
283
357
|
* Construction context for {@link createServerSharedModule}.
|
|
284
358
|
*
|
|
@@ -327,6 +401,8 @@ export function createServerSharedModule(
|
|
|
327
401
|
ServiceRegistry: services => new ExtendedServiceRegistry<ServerLanguageServices>(services),
|
|
328
402
|
Logger: () => new NoopLogger(),
|
|
329
403
|
Tracer: services => new ServerTracer(services.Logger, services.Clock),
|
|
404
|
+
ServerLocale: services => new DefaultServerLocale(services),
|
|
405
|
+
MessageRenderer: services => new DefaultMessageRenderer(services),
|
|
330
406
|
// Bind Langium's per-grammar-rule / per-`$type` parse/link/validate
|
|
331
407
|
// profiler (the data the framework's own `Tracer`/`ProfileSession` passes
|
|
332
408
|
// cannot produce), routed through our `Logger` and debug-gated. At the
|
|
@@ -392,12 +468,16 @@ export function createServerSharedModule(
|
|
|
392
468
|
// document retains serialized text via the per-language Serializer,
|
|
393
469
|
// making it re-read-safe through the virtual-aware FileSystemProvider.
|
|
394
470
|
LangiumDocumentFactory: services => new HydraniumLangiumDocumentFactory(services),
|
|
471
|
+
// Langium's default routes through no identity seam and treats every
|
|
472
|
+
// failed load alike, so leaving this unbound opts a server out of both
|
|
473
|
+
// with nothing to signal it.
|
|
474
|
+
LangiumDocuments: services => new HydraniumLangiumDocuments(services),
|
|
395
475
|
DocumentBuilder: services => new HydraniumDocumentBuilder(services),
|
|
396
476
|
// The write-lock scope this marks is inert unless a host installs a
|
|
397
477
|
// scope tracker — `@hydranium/core/node` does.
|
|
398
478
|
WorkspaceLock: () => new HydraniumWorkspaceLock(),
|
|
399
479
|
ProjectManager: services => new SingleProjectManager(services),
|
|
400
|
-
SelfSaveRegistry: services => new
|
|
480
|
+
SelfSaveRegistry: services => new DefaultSelfSaveRegistry(services),
|
|
401
481
|
DocumentUriPolicy: () => new DefaultDocumentUriPolicy(),
|
|
402
482
|
// Writable filesystem with two paths:
|
|
403
483
|
// - Adopter passed `context.fileSystemProvider` and it returned a
|
|
@@ -421,20 +501,20 @@ export function createServerSharedModule(
|
|
|
421
501
|
}
|
|
422
502
|
return new DefaultEmptyFileSystemProvider(services);
|
|
423
503
|
},
|
|
424
|
-
AstDocumentManager: services => new
|
|
504
|
+
AstDocumentManager: services => new DefaultAstDocumentManager(services),
|
|
425
505
|
// Eagerly constructed, so its build-phase listeners attach before the
|
|
426
506
|
// first build.
|
|
427
|
-
BuildPipelineIntegration: services => new
|
|
507
|
+
BuildPipelineIntegration: services => new DefaultBuildPipelineIntegration(services),
|
|
428
508
|
BuildPhasePassService: services => new DefaultBuildPhasePassService(services),
|
|
429
509
|
// Eagerly constructed, so its `Validated` pass registers before the
|
|
430
510
|
// first build.
|
|
431
|
-
CstResidencyService: services => new
|
|
511
|
+
CstResidencyService: services => new DefaultCstResidencyService(services)
|
|
432
512
|
},
|
|
433
513
|
model: {
|
|
434
514
|
// Generic walker — adopters with a typed `$type → wire shape` overlay
|
|
435
515
|
// rebind this slot with a subclass.
|
|
436
|
-
TransferEncoder: services => new
|
|
437
|
-
ModelService: services => new
|
|
516
|
+
TransferEncoder: services => new DefaultTransferEncoder(services),
|
|
517
|
+
ModelService: services => new DefaultModelService(services)
|
|
438
518
|
},
|
|
439
519
|
// Empty default so `services.buildPhasePasses` always resolves (Langium
|
|
440
520
|
// throws on access to an unbound slot). The framework's own integrity /
|
|
@@ -59,6 +59,16 @@ export interface NameProviderOptions {
|
|
|
59
59
|
* an `id` field) override explicitly; the explicit override at
|
|
60
60
|
* the adopter side documents the grammar's convention rather than
|
|
61
61
|
* burying it in a framework default.
|
|
62
|
+
*
|
|
63
|
+
* **The override is not optional where `name` is a display LABEL rather
|
|
64
|
+
* than an identifier**, and getting it wrong is quiet at first and then
|
|
65
|
+
* loud. A `name=STRING` holding `"Order.Line"` is legitimate content, but
|
|
66
|
+
* the default reads it as the identifier: `getName` composes a qualified
|
|
67
|
+
* name whose segments cannot be told apart from the label's own dots, and
|
|
68
|
+
* `nameSeparatorCheck` reports `hydranium/core/separator-in-name` on every
|
|
69
|
+
* such node. That diagnostic then names the wrong remedy — it asks for a
|
|
70
|
+
* different character, when the fix is to point this option at the property
|
|
71
|
+
* that really is the identifier.
|
|
62
72
|
*/
|
|
63
73
|
readonly nameProperties?: readonly string[];
|
|
64
74
|
}
|
|
@@ -8,10 +8,39 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import { type AstNode, type ValidationAcceptor, type ValidationChecks } from '@hydranium/langium';
|
|
11
|
+
import { defineMessage } from '@hydranium/protocol';
|
|
12
|
+
import { acceptMessage } from '../../messages/carriers.js';
|
|
11
13
|
import { type ServerLanguageServices } from '../language-module.js';
|
|
12
14
|
import { type ValidationCheckContribution, type ValidationCheckRegistry } from '../validation/validation-contribution.js';
|
|
13
15
|
import type { NameProvider } from './name-provider.js';
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* A name value that collides with the qualified-name separator.
|
|
19
|
+
*
|
|
20
|
+
* **The sentence addresses a MODELLER, and the remedy it names is theirs**:
|
|
21
|
+
* where `name` really is the identifier, a different character is the fix and
|
|
22
|
+
* the only one available to whoever is typing.
|
|
23
|
+
*
|
|
24
|
+
* **The adopter-facing failure is a different one, and this message cannot
|
|
25
|
+
* carry it.** With `nameProperties` left at its default `['name']` over a
|
|
26
|
+
* grammar where `name` is a display LABEL — a `STRING` legitimately holding
|
|
27
|
+
* `"Order.Line"` — this fires on every dotted label at once, and "use a
|
|
28
|
+
* different character" is then advice nobody can act on: the content is
|
|
29
|
+
* correct and the configuration is not. A burst of this diagnostic across
|
|
30
|
+
* unrelated nodes is that misconfiguration, not a naming problem, and the fix
|
|
31
|
+
* is `nameProperties`. Widening the sentence to say both was rejected: a
|
|
32
|
+
* modeller cannot act on a DI option, and naming one in an editor squiggle
|
|
33
|
+
* teaches the wrong audience.
|
|
34
|
+
*
|
|
35
|
+
* Unreachable from a grammar whose identifier charset excludes the separator,
|
|
36
|
+
* which is every example here and the reference adopter — so it reads as dead
|
|
37
|
+
* and is not: it guards the configuration, not the content.
|
|
38
|
+
*/
|
|
39
|
+
export const SEPARATOR_IN_NAME = defineMessage(
|
|
40
|
+
'hydranium/core/separator-in-name',
|
|
41
|
+
"Name '{name}' contains the configured name separator '{separator}', which is reserved for qualified-name composition — use a different character."
|
|
42
|
+
);
|
|
43
|
+
|
|
15
44
|
/**
|
|
16
45
|
* Per-node validation check: flag name-bearing AST nodes whose name
|
|
17
46
|
* value contains the configured name separator. Such values produce
|
|
@@ -38,11 +67,11 @@ export function nameSeparatorCheck(nameProvider: NameProvider): (node: AstNode,
|
|
|
38
67
|
}
|
|
39
68
|
const separator = nameProvider.nameSeparator;
|
|
40
69
|
if (ownName.includes(separator)) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
);
|
|
70
|
+
// `{ node }` rather than `{ node, property: 'name' }`: moving the
|
|
71
|
+
// squiggle onto the name property is a behaviour change to the one
|
|
72
|
+
// diagnostic every adopter receives by default, so it is decided on its
|
|
73
|
+
// own merits rather than carried in by a mechanical migration.
|
|
74
|
+
acceptMessage(accept, 'error', SEPARATOR_IN_NAME, { node }, { name: ownName, separator });
|
|
46
75
|
}
|
|
47
76
|
};
|
|
48
77
|
}
|