@hydranium/core 1.0.0-next.94 → 1.0.0-next.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/lib/index.d.ts +1 -0
  2. package/lib/index.d.ts.map +1 -1
  3. package/lib/index.js +1 -0
  4. package/lib/index.js.map +1 -1
  5. package/lib/langium/document-builder/build-pipeline-integration.d.ts +0 -9
  6. package/lib/langium/document-builder/build-pipeline-integration.d.ts.map +1 -1
  7. package/lib/langium/document-builder/build-pipeline-integration.js +4 -19
  8. package/lib/langium/document-builder/build-pipeline-integration.js.map +1 -1
  9. package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
  10. package/lib/langium/integrity/integrity-service.js +8 -1
  11. package/lib/langium/integrity/integrity-service.js.map +1 -1
  12. package/lib/langium/language-module.d.ts +25 -0
  13. package/lib/langium/language-module.d.ts.map +1 -1
  14. package/lib/langium/language-module.js +14 -0
  15. package/lib/langium/language-module.js.map +1 -1
  16. package/lib/langium/model-service/model-service.d.ts +30 -18
  17. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  18. package/lib/langium/model-service/model-service.js +41 -1
  19. package/lib/langium/model-service/model-service.js.map +1 -1
  20. package/lib/langium/residency/cst-residency-service.d.ts +4 -6
  21. package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
  22. package/lib/langium/residency/cst-residency-service.js.map +1 -1
  23. package/lib/langium/serialization/abstract-serializer.d.ts +11 -20
  24. package/lib/langium/serialization/abstract-serializer.d.ts.map +1 -1
  25. package/lib/langium/serialization/abstract-serializer.js +12 -21
  26. package/lib/langium/serialization/abstract-serializer.js.map +1 -1
  27. package/lib/langium/trivia/comment-preserver.d.ts +423 -0
  28. package/lib/langium/trivia/comment-preserver.d.ts.map +1 -0
  29. package/lib/langium/trivia/comment-preserver.js +906 -0
  30. package/lib/langium/trivia/comment-preserver.js.map +1 -0
  31. package/lib/langium/trivia/document-ending-preserver.d.ts +43 -0
  32. package/lib/langium/trivia/document-ending-preserver.d.ts.map +1 -0
  33. package/lib/langium/trivia/document-ending-preserver.js +48 -0
  34. package/lib/langium/trivia/document-ending-preserver.js.map +1 -0
  35. package/lib/langium/trivia/index.d.ts +14 -0
  36. package/lib/langium/trivia/index.d.ts.map +1 -0
  37. package/lib/langium/trivia/index.js +14 -0
  38. package/lib/langium/trivia/index.js.map +1 -0
  39. package/lib/langium/trivia/trivia-contribution.d.ts +37 -0
  40. package/lib/langium/trivia/trivia-contribution.d.ts.map +1 -0
  41. package/lib/langium/trivia/trivia-contribution.js +10 -0
  42. package/lib/langium/trivia/trivia-contribution.js.map +1 -0
  43. package/lib/langium/trivia/trivia-preserver.d.ts +50 -0
  44. package/lib/langium/trivia/trivia-preserver.d.ts.map +1 -0
  45. package/lib/langium/trivia/trivia-preserver.js +10 -0
  46. package/lib/langium/trivia/trivia-preserver.js.map +1 -0
  47. package/lib/langium/trivia/trivia-service.d.ts +70 -0
  48. package/lib/langium/trivia/trivia-service.d.ts.map +1 -0
  49. package/lib/langium/trivia/trivia-service.js +56 -0
  50. package/lib/langium/trivia/trivia-service.js.map +1 -0
  51. package/lib/testing/node/scratch-workspace.js +2 -2
  52. package/package.json +5 -5
  53. package/src/index.ts +1 -0
  54. package/src/langium/document-builder/build-pipeline-integration.ts +8 -21
  55. package/src/langium/integrity/integrity-service.ts +9 -1
  56. package/src/langium/language-module.ts +38 -0
  57. package/src/langium/model-service/model-service.ts +58 -19
  58. package/src/langium/residency/cst-residency-service.ts +4 -6
  59. package/src/langium/serialization/abstract-serializer.ts +13 -32
  60. package/src/langium/trivia/comment-preserver.ts +1100 -0
  61. package/src/langium/trivia/document-ending-preserver.ts +55 -0
  62. package/src/langium/trivia/index.ts +14 -0
  63. package/src/langium/trivia/trivia-contribution.ts +39 -0
  64. package/src/langium/trivia/trivia-preserver.ts +54 -0
  65. package/src/langium/trivia/trivia-service.ts +99 -0
  66. package/src/testing/node/scratch-workspace.ts +2 -2
@@ -0,0 +1,55 @@
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
+ import { type LangiumDocument } from '@hydranium/langium';
11
+ import { type TriviaContribution, type TriviaRegistry } from './trivia-contribution.js';
12
+ import { type TriviaPreserver } from './trivia-preserver.js';
13
+
14
+ /**
15
+ * Carries the whitespace a document ended with across a write.
16
+ *
17
+ * **Normalising this to one final newline rewrites files nobody asked to
18
+ * restyle.** A document may legitimately end in blank lines, or deliberately in
19
+ * none, and a write that changed one field is not the place to decide it should
20
+ * not. A serializer cannot make the call either — it has no prior file to
21
+ * consult — which is why the decision lives on the write path.
22
+ *
23
+ * Line terminators are matched to the body the serializer emits, which is the
24
+ * one thing here that cannot be preserved: keeping them verbatim leaves a
25
+ * rewritten CRLF document with its only CRLFs at the very end.
26
+ */
27
+ export class DocumentEndingPreserver implements TriviaPreserver<string> {
28
+ readonly id = 'document-ending';
29
+ readonly label = 'Document Ending';
30
+
31
+ extract(document: LangiumDocument): string {
32
+ const source = document.textDocument.getText();
33
+ const ending = source.slice(source.trimEnd().length);
34
+ return ending.includes('\r') ? ending.replace(/\r\n/g, '\n') : ending;
35
+ }
36
+
37
+ /**
38
+ * **Override for a grammar whose CONTENT can end in whitespace** — a
39
+ * block-scalar property as the document's last value, say. The trim cannot
40
+ * tell a blank line the serializer emitted as part of a value from one that
41
+ * is merely the end of the file, so it removes both and restores the ending
42
+ * the old document had. A document whose final value just gained trailing
43
+ * blank lines therefore loses them.
44
+ */
45
+ apply(serialized: string, trivia: string): string {
46
+ return serialized.trimEnd() + trivia;
47
+ }
48
+ }
49
+
50
+ /** Registers {@link DocumentEndingPreserver}; bound at `trivia.preservers.documentEnding`. */
51
+ export class DocumentEndingPreserverContribution implements TriviaContribution {
52
+ registerTriviaPreservers(registry: TriviaRegistry): void {
53
+ registry.register(new DocumentEndingPreserver());
54
+ }
55
+ }
@@ -0,0 +1,14 @@
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
+ export * from './comment-preserver.js';
11
+ export * from './document-ending-preserver.js';
12
+ export * from './trivia-contribution.js';
13
+ export * from './trivia-preserver.js';
14
+ export * from './trivia-service.js';
@@ -0,0 +1,39 @@
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
+ import { type Disposable } from 'vscode-languageserver';
11
+ import { type TriviaPreserver } from './trivia-preserver.js';
12
+
13
+ /**
14
+ * Registry handed to a {@link TriviaContribution}. Implemented by the trivia
15
+ * service; a contribution receives it and registers one or many preservers.
16
+ * Doubles as the low-level imperative API for the rare runtime-dynamic case.
17
+ */
18
+ export interface TriviaRegistry {
19
+ register(preserver: TriviaPreserver): Disposable;
20
+ }
21
+
22
+ /**
23
+ * Declarative registration of trivia preservers. Bound under the module's
24
+ * `trivia.preservers` contribution group; the trivia service reads its own
25
+ * group at construction and calls this method, handing itself in as the
26
+ * registry.
27
+ *
28
+ * The framework binds one sub-key per preserver it ships rather than one for
29
+ * all of them, so an adopter replacing `comments` — a grammar identifying its
30
+ * nodes by something no name property holds — keeps the rest. Langium's
31
+ * deep-merge is last-wins on same-key leaves, so binding a sub-key REPLACES the
32
+ * framework's contribution under it rather than adding to it.
33
+ *
34
+ * The domain-qualified method name lets one cross-cutting class implement
35
+ * several contribution interfaces without method collision.
36
+ */
37
+ export interface TriviaContribution {
38
+ registerTriviaPreservers(registry: TriviaRegistry): void;
39
+ }
@@ -0,0 +1,54 @@
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
+ import { type LangiumDocument, type URI } from '@hydranium/langium';
11
+ import { type RegistryItem } from '../../util/registry.js';
12
+
13
+ /**
14
+ * One kind of trivia — what a document carries that its model does not, so a
15
+ * write that re-serializes from the model would otherwise lose it.
16
+ *
17
+ * **Two phases, because there is a suspension point between them.** The write
18
+ * path extracts, awaits the serializer, then applies; and the preserver is a
19
+ * per-language singleton, so two concurrent writes would clobber each other if
20
+ * it held the extracted value itself. `T` therefore travels through the caller.
21
+ *
22
+ * Declared with METHODS rather than arrow properties: an implementation
23
+ * narrowing `T` stays assignable only under method-parameter bivariance, which
24
+ * arrow properties lose.
25
+ */
26
+ export interface TriviaPreserver<T = unknown> extends RegistryItem {
27
+ /**
28
+ * Take this preserver's trivia off `document`, while it still holds the text
29
+ * the write is about to replace.
30
+ */
31
+ extract(document: LangiumDocument): T;
32
+
33
+ /**
34
+ * Put `trivia` back into `serialized`, returning the text the NEXT preserver
35
+ * sees — the chain is a fold, so an implementation must derive its positions
36
+ * from the text it is handed rather than from the document it extracted from.
37
+ */
38
+ apply(serialized: string, trivia: T, uri: URI): string;
39
+ }
40
+
41
+ /**
42
+ * One preserver's output, kept beside the preserver that produced it.
43
+ *
44
+ * Pairing them is what lets the chain stay agnostic: it never learns what a
45
+ * payload IS, only which preserver knows how to apply it, so a preserver can
46
+ * use whatever shape suits it without the service naming that shape.
47
+ */
48
+ export interface ExtractedTrivia {
49
+ readonly preserver: TriviaPreserver;
50
+ readonly trivia: unknown;
51
+ }
52
+
53
+ /** Everything the registered preservers took off one document, in the order they ran. */
54
+ export type DocumentTrivia = readonly ExtractedTrivia[];
@@ -0,0 +1,99 @@
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
+ import { type Tracer } from '@hydranium/protocol';
11
+ import { type LangiumDocument, type URI } from '@hydranium/langium';
12
+ import { type Disposable } from 'vscode-languageserver';
13
+ import { type LogNameOptions } from '../diagnostics/logger.js';
14
+ import { type HydraniumLanguageServices } from '../language-module.js';
15
+ import { Registry } from '../../util/registry.js';
16
+ import { type TriviaRegistry } from './trivia-contribution.js';
17
+ import { type DocumentTrivia, type TriviaPreserver } from './trivia-preserver.js';
18
+
19
+ /**
20
+ * Public contract for the per-language trivia runner. Extends the
21
+ * {@link TriviaRegistry} contributions register through with the entry points
22
+ * the write paths call around serialization.
23
+ *
24
+ * Adopter overrides go through {@link DefaultTriviaService}; the interface keeps
25
+ * the public API stable while the registry stays `protected` on the class.
26
+ */
27
+ export interface TriviaService extends TriviaRegistry {
28
+ /** Remove a preserver by id. Returns `true` if one was removed. */
29
+ unregister(id: string): boolean;
30
+
31
+ /**
32
+ * Run every registered preserver's `extract` over `document`, in priority
33
+ * order, while it still holds the text the write is about to replace.
34
+ */
35
+ extract(document: LangiumDocument): DocumentTrivia;
36
+
37
+ /**
38
+ * Fold `serialized` through every extracted entry, handing each preserver
39
+ * back its own payload, and return the text to write.
40
+ */
41
+ apply(serialized: string, trivia: DocumentTrivia, uri: URI): string;
42
+ }
43
+
44
+ /** Construction options for {@link DefaultTriviaService}. */
45
+ export type TriviaServiceOptions = LogNameOptions;
46
+
47
+ /**
48
+ * Default {@link TriviaService}: an ordered registry of preservers, run around
49
+ * the serializer on every structured write.
50
+ *
51
+ * **An empty registry is a no-op**, which is how preservation is switched off —
52
+ * there is no enabled flag, and a preserver that is not registered costs
53
+ * nothing rather than being asked and declining.
54
+ *
55
+ * **Per-language service.** Comment terminals come from the grammar, so this
56
+ * cannot be shared; `ModelService` resolves it via
57
+ * `ServiceRegistry.getServices(uri)` so a multi-grammar workspace routes each
58
+ * write to the right preservers.
59
+ */
60
+ export class DefaultTriviaService implements TriviaService {
61
+ /** Registry of preservers, keyed by id, iterated in priority order. */
62
+ protected readonly preservers = new Registry<TriviaPreserver>();
63
+ protected readonly tracer: Tracer;
64
+
65
+ constructor(
66
+ protected readonly services: HydraniumLanguageServices,
67
+ options: TriviaServiceOptions = {}
68
+ ) {
69
+ this.tracer = services.shared.Tracer.for(options.logName ?? 'Trivia').trace('instantiated');
70
+
71
+ // Optional chaining tolerates incomplete test stubs; production wiring
72
+ // always provides the slot via `createServerLanguageModule`.
73
+ const contributions = services.trivia?.preservers ?? {};
74
+ for (const contribution of Object.values(contributions)) {
75
+ contribution.registerTriviaPreservers(this);
76
+ }
77
+ }
78
+
79
+ register(preserver: TriviaPreserver): Disposable {
80
+ return this.preservers.register(preserver);
81
+ }
82
+
83
+ unregister(id: string): boolean {
84
+ return this.preservers.unregister(id);
85
+ }
86
+
87
+ extract(document: LangiumDocument): DocumentTrivia {
88
+ return this.preservers.all().map(preserver => ({ preserver, trivia: preserver.extract(document) }));
89
+ }
90
+
91
+ /**
92
+ * Fold, not map: each preserver sees what the ones before it produced, so a
93
+ * pair that touch the same region resolve by priority rather than by
94
+ * whichever happened to register first.
95
+ */
96
+ apply(serialized: string, trivia: DocumentTrivia, uri: URI): string {
97
+ return trivia.reduce((text, entry) => entry.preserver.apply(text, entry.trivia, uri), serialized);
98
+ }
99
+ }
@@ -21,8 +21,8 @@
21
21
  * committed sample workspace can **rewrite the sample files**, after which
22
22
  * every later run asserts against already-repaired input: a suite that passes
23
23
  * while testing nothing. The same applies to a fixture built in place, which
24
- * additionally loses its explanatory comments, because the write-back goes
25
- * through the serializer.
24
+ * additionally comes back in the serializer's layout, because the write-back
25
+ * goes through it.
26
26
  *
27
27
  * The second reason is cleanup. Hand-rolled `mkdtempSync` without a matching
28
28
  * `rmSync` leaks a directory per test run into the OS temp dir, which nobody