@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
@@ -28,6 +28,10 @@ import { DefaultReferenceBuilder, type ReferenceBuilder } from './scope/referenc
28
28
  import { type ScopeExtensionContribution } from './scope/scope-extension-contribution.js';
29
29
  import { DefaultScopeExtensionService, type ScopeExtensionService } from './scope/scope-extension-service.js';
30
30
  import { type Serializer } from './serialization/serializer.js';
31
+ import { type TriviaContribution } from './trivia/trivia-contribution.js';
32
+ import { type TriviaService, DefaultTriviaService } from './trivia/trivia-service.js';
33
+ import { CommentPreserverContribution } from './trivia/comment-preserver.js';
34
+ import { DocumentEndingPreserverContribution } from './trivia/document-ending-preserver.js';
31
35
  import { type UpdateRewriteContribution } from './update-rewrite/update-rewrite-contribution.js';
32
36
  import { DefaultUpdateRewriteService, type UpdateRewriteService } from './update-rewrite/update-rewrite-service.js';
33
37
  import { HydraniumDocumentValidator } from './validation/document-validator.js';
@@ -181,6 +185,29 @@ export interface ServerAddedServices {
181
185
  */
182
186
  rules: Record<string, IntegrityRuleContribution>;
183
187
  };
188
+ trivia: {
189
+ /**
190
+ * Runs the registered trivia preservers around a write, so what a
191
+ * document carries that its model does not — its comments, the whitespace
192
+ * it ended with — survives being re-serialized from that model.
193
+ *
194
+ * A separate group from {@link Serializer} because a preserver needs the
195
+ * document being overwritten, which a serializer is never given:
196
+ * `serializeAst` could reach one through its model, but
197
+ * `serializeTransfer` receives a plain wire object with no document at
198
+ * all, so folding this in would carry trivia on one write path and
199
+ * silently not on the other.
200
+ */
201
+ TriviaService: TriviaService;
202
+ /**
203
+ * Declarative {@link TriviaContribution} group. Distinct sub-keys
204
+ * accumulate across framework + adopter modules via Langium's deep-merge;
205
+ * binding one the framework already uses REPLACES it. The framework ships
206
+ * a sub-key per preserver, so an adopter replacing `comments` keeps
207
+ * `documentEnding` and whatever else ships beside it.
208
+ */
209
+ preservers: Record<string, TriviaContribution>;
210
+ };
184
211
  updateRewrite: {
185
212
  /**
186
213
  * Per-language runner for transfer-model rewrites applied on the
@@ -334,6 +361,17 @@ export function createServerLanguageModule(
334
361
  serializer: {
335
362
  Serializer: () => new UnboundSerializer()
336
363
  },
364
+ // Bound with a sub-key per preserver so an adopter can replace one and
365
+ // keep the other. Adopters MUST avoid these two keys for their own
366
+ // contributions unless replacement is what they mean — Langium's
367
+ // deep-merge is last-wins on same-key leaves.
368
+ trivia: {
369
+ TriviaService: services => new DefaultTriviaService(services),
370
+ preservers: {
371
+ comments: services => new CommentPreserverContribution(services),
372
+ documentEnding: () => new DocumentEndingPreserverContribution()
373
+ }
374
+ },
337
375
  // The per-URI serialise / re-parse path only fires when a rule mutates,
338
376
  // so the no-op default is safe even before adopters bind a real
339
377
  // serializer / parser.
@@ -134,6 +134,10 @@ export interface ModelServiceOptions extends LogNameOptions {
134
134
  }
135
135
 
136
136
  /**
137
+ * The seam every non-LSP head talks to: the data server, the GLSP head and an
138
+ * adopter's own services reach documents through this slot rather than through
139
+ * the workspace stores.
140
+ *
137
141
  * In-process facade over the framework's document plumbing
138
142
  * (`HydraniumTextDocuments`, `LangiumDocuments`,
139
143
  * `DocumentBuilder`, `WritableFileSystemProvider`). Owns the
@@ -192,24 +196,6 @@ export interface ModelServiceOptions extends LogNameOptions {
192
196
  * by URI happens here so consumers can subscribe per-document without
193
197
  * implementing the URI gate at each callsite.
194
198
  *
195
- * **Generic parameters.**
196
- * - `TAst` — the AST root type each consumer expects on the returned
197
- * {@link AstDocument}. Constrained to {@link AstNode}.
198
- * - `TDiagnostic` — the AST-layer diagnostic: whatever the build left on
199
- * `LangiumDocument.diagnostics`, carried through on the returned
200
- * {@link AstDocument}. Defaults to {@link AstDiagnostic}.
201
- * **Not the `TransferEncoder`'s parameter of the same name**, which is
202
- * that encoder's OUTPUT and so names the wire shape. This one names its
203
- * input, and an adopter binds the two to different types.
204
- * - `TTransfer` — transfer-model root accepted by `update` / `save`
205
- * args. Constrained to {@link TransferElement}. Defaults to the
206
- * structural base.
207
- */
208
- /**
209
- * The seam every non-LSP head talks to: the data server, the GLSP head and an
210
- * adopter's own services reach documents through this slot rather than through
211
- * the workspace stores.
212
- *
213
199
  * **Two families, and the distinction matters more than the names suggest.**
214
200
  * `waitFor*` is a pure wait — it never triggers a build, so a caller waiting on
215
201
  * a document no build has touched waits until something else builds it.
@@ -232,6 +218,19 @@ export interface ModelServiceOptions extends LogNameOptions {
232
218
  * direction is the safe one: the alternative permits stale reads silently. A
233
219
  * member taking a phase as a PARAMETER cannot judge statically and so returns
234
220
  * `TDiagnostic`, leaving the choice to the caller.
221
+ *
222
+ * **Generic parameters.**
223
+ * - `TAst` — the AST root type each consumer expects on the returned
224
+ * {@link AstDocument}. Constrained to {@link AstNode}.
225
+ * - `TDiagnostic` — the AST-layer diagnostic: whatever the build left on
226
+ * `LangiumDocument.diagnostics`, carried through on the returned
227
+ * {@link AstDocument}. Defaults to {@link AstDiagnostic}.
228
+ * **Not the `TransferEncoder`'s parameter of the same name**, which is
229
+ * that encoder's OUTPUT and so names the wire shape. This one names its
230
+ * input, and an adopter binds the two to different types.
231
+ * - `TTransfer` — transfer-model root accepted by `update` / `save`
232
+ * args. Constrained to {@link TransferElement}. Defaults to the
233
+ * structural base.
235
234
  */
236
235
  export interface ModelService<
237
236
  TAst extends AstNode,
@@ -1175,7 +1174,47 @@ export class DefaultModelService<
1175
1174
  return model;
1176
1175
  }
1177
1176
  const rewritten = await this.rewriteModel(uri, model, cancelToken);
1178
- return this.serialize(uri, rewritten);
1177
+ const target = UriUtils.toUri(uri);
1178
+ const trivia = this.services.ServiceRegistry?.getServices(target)?.trivia?.TriviaService;
1179
+ // Extracted BEFORE serializing, so it reads the document the write is
1180
+ // about to replace rather than whatever a concurrent build left behind.
1181
+ let document = this.services.workspace.LangiumDocuments.getDocument(target);
1182
+ if (trivia !== undefined && document === undefined) {
1183
+ const source = await this.textToTakeTriviaFrom(uri, target);
1184
+ if (source !== undefined) {
1185
+ document = this.services.workspace.LangiumDocumentFactory.fromString(source, target);
1186
+ }
1187
+ }
1188
+ const extracted = trivia !== undefined && document !== undefined ? trivia.extract(document) : undefined;
1189
+ const serialized = await this.serialize(uri, rewritten);
1190
+ return extracted === undefined ? serialized : trivia!.apply(serialized, extracted, target);
1191
+ }
1192
+
1193
+ /**
1194
+ * The text a write into an unloaded document should take its trivia from —
1195
+ * the open editor's if one holds it, the file's otherwise, and `undefined`
1196
+ * when neither can supply it.
1197
+ *
1198
+ * **A read that fails answers `undefined` rather than throwing.** `getDocument`
1199
+ * is a store read, so an ordinary create answers nothing here and must still
1200
+ * write; letting a permission error or a path that turned into a directory
1201
+ * escape would fail the user's write instead, having found nothing to preserve
1202
+ * in a file the write is about to replace anyway. Losing the comments is
1203
+ * recoverable, and traced; losing the write is neither.
1204
+ */
1205
+ protected async textToTakeTriviaFrom(uri: string, target: URI): Promise<string | undefined> {
1206
+ const open = this.services.workspace.TextDocuments.get(uri)?.getText();
1207
+ if (open !== undefined) {
1208
+ return open;
1209
+ }
1210
+ const fileSystem = this.services.workspace.FileSystemProvider;
1211
+ try {
1212
+ return (await fileSystem.exists(target)) ? await fileSystem.readFile(target) : undefined;
1213
+ } catch (err: unknown) {
1214
+ const detail = err instanceof Error ? err.message : String(err);
1215
+ this.tracer.withUri(uri).debug(`Could not read the file to take trivia from; writing as emitted. ${detail}`);
1216
+ return undefined;
1217
+ }
1179
1218
  }
1180
1219
 
1181
1220
  /**
@@ -185,8 +185,10 @@ function rehydrateCst(document: LangiumDocument, factory: LangiumDocumentFactory
185
185
  }
186
186
 
187
187
  /**
188
- * Residency policy that reclaims memory by shedding the concrete syntax tree
189
- * (CST) of closed documents while keeping their AST resident. Each build refreshes
188
+ * Keeps a document's CST available to the readers that need one, shedding it
189
+ * for documents that have gone idle and re-grafting on demand.
190
+ *
191
+ * Each build refreshes
190
192
  * the idle window of the documents in its batch; when a closed, sheddable
191
193
  * document's window elapses, every AST node's `$cstNode` and every reference's
192
194
  * `$refNode` are nulled, and the CST root, `Range`s and `Position`s become
@@ -231,10 +233,6 @@ function rehydrateCst(document: LangiumDocument, factory: LangiumDocumentFactory
231
233
  * `NameProvider.getNameNode` chokepoint, the comment provider, and
232
234
  * `HydraniumLangiumDocuments.getOrCreateDocument`.
233
235
  */
234
- /**
235
- * Keeps a document's CST available to the readers that need one, shedding it
236
- * for documents that have gone idle and re-grafting on demand.
237
- */
238
236
  export interface CstResidencyService {
239
237
  /**
240
238
  * Ensure `document` has a CST, re-parsing its retained text if it was shed.
@@ -7,7 +7,7 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import { isPromiseLike, type MaybePromise, type Tracer, type TransferElement } from '@hydranium/protocol';
10
+ import { type MaybePromise, type Tracer, type TransferElement } from '@hydranium/protocol';
11
11
  import { type AstNode, type AstReflection, type GenericAstNode, isAstNode } from '@hydranium/langium';
12
12
  import { type LogNameOptions } from '../diagnostics/logger.js';
13
13
  import { type HydraniumLanguageServices } from '../language-module.js';
@@ -66,16 +66,6 @@ export interface AbstractSerializerOptions extends LogNameOptions {
66
66
  * the extra property carries a value.
67
67
  */
68
68
  readonly referenceWrapperTypes?: ReadonlyMap<string, string>;
69
- /**
70
- * Whether {@link AbstractSerializer.serializeAst} trims trailing whitespace
71
- * from the final output (leading whitespace is always trimmed). Defaults to
72
- * `true` — the conventional "no trailing blank lines" document shape. Set to
73
- * `false` for grammars whose values may legitimately END the document with
74
- * blank lines that must survive a round-trip — e.g. a multi-line block-scalar
75
- * property (see `YamlSerializerOptions.blockScalarProperties`),
76
- * where a trailing-trim would silently eat user-typed blank lines.
77
- */
78
- readonly trimTrailingWhitespace?: boolean;
79
69
  }
80
70
 
81
71
  /**
@@ -139,29 +129,20 @@ export abstract class AbstractSerializer<
139
129
  * Entry point for AST-shape inputs. Typically the grammar root, but any
140
130
  * AST node works — the walker is recursive and shape-agnostic.
141
131
  *
142
- * Preserves the sync fast path: when every subclass override and every
143
- * adopter hook is sync, the return is a bare `string` (no Promise
144
- * allocation, no microtask). The async branch fires only if at least one
145
- * recursive `serializeNode` call resolved to a Promise.
132
+ * **Emits the model and nothing else — do not trim here.** Trailing
133
+ * whitespace belongs to the author, and a write path holding the prior
134
+ * document restores it from there; trimming at this end deletes it before
135
+ * that restore sees it. A serializer has no prior document to consult, so it
136
+ * cannot tell a deliberate blank line from an accidental one.
137
+ *
138
+ * **Nothing trims what a subclass emits**, at either end. Emitting leading
139
+ * whitespace therefore puts it in the file, where no write path removes it.
140
+ *
141
+ * Returns whatever `serializeNode` returns, so a fully sync subclass keeps a
142
+ * bare `string` with no promise allocation.
146
143
  */
147
144
  serializeAst(model: TAst): MaybePromise<string> {
148
- const result = this.serializeNode(model, 0);
149
- if (isPromiseLike(result)) {
150
- return result.then(text => this.trimSerialized(text));
151
- }
152
- return this.trimSerialized(result);
153
- }
154
-
155
- /**
156
- * Trim the final serialized output. Leading whitespace is always removed;
157
- * trailing whitespace is removed unless {@link AbstractSerializerOptions.trimTrailingWhitespace}
158
- * is `false` (which preserves document-ending blank lines, e.g. inside a block scalar).
159
- */
160
- protected trimSerialized(text: string | undefined): string {
161
- if (text === undefined) {
162
- return '';
163
- }
164
- return this.options.trimTrailingWhitespace === false ? text.trimStart() : text.trim();
145
+ return this.serializeNode(model, 0);
165
146
  }
166
147
 
167
148
  /**