@hydranium/core 1.0.0-next.95 → 1.0.0-next.97

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 (61) 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/integrity/integrity-service.d.ts.map +1 -1
  6. package/lib/langium/integrity/integrity-service.js +8 -1
  7. package/lib/langium/integrity/integrity-service.js.map +1 -1
  8. package/lib/langium/language-module.d.ts +25 -0
  9. package/lib/langium/language-module.d.ts.map +1 -1
  10. package/lib/langium/language-module.js +14 -0
  11. package/lib/langium/language-module.js.map +1 -1
  12. package/lib/langium/model-service/model-service.d.ts +30 -18
  13. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  14. package/lib/langium/model-service/model-service.js +41 -1
  15. package/lib/langium/model-service/model-service.js.map +1 -1
  16. package/lib/langium/residency/cst-residency-service.d.ts +4 -6
  17. package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
  18. package/lib/langium/residency/cst-residency-service.js.map +1 -1
  19. package/lib/langium/serialization/abstract-serializer.d.ts +11 -20
  20. package/lib/langium/serialization/abstract-serializer.d.ts.map +1 -1
  21. package/lib/langium/serialization/abstract-serializer.js +12 -21
  22. package/lib/langium/serialization/abstract-serializer.js.map +1 -1
  23. package/lib/langium/trivia/comment-preserver.d.ts +423 -0
  24. package/lib/langium/trivia/comment-preserver.d.ts.map +1 -0
  25. package/lib/langium/trivia/comment-preserver.js +906 -0
  26. package/lib/langium/trivia/comment-preserver.js.map +1 -0
  27. package/lib/langium/trivia/document-ending-preserver.d.ts +43 -0
  28. package/lib/langium/trivia/document-ending-preserver.d.ts.map +1 -0
  29. package/lib/langium/trivia/document-ending-preserver.js +48 -0
  30. package/lib/langium/trivia/document-ending-preserver.js.map +1 -0
  31. package/lib/langium/trivia/index.d.ts +14 -0
  32. package/lib/langium/trivia/index.d.ts.map +1 -0
  33. package/lib/langium/trivia/index.js +14 -0
  34. package/lib/langium/trivia/index.js.map +1 -0
  35. package/lib/langium/trivia/trivia-contribution.d.ts +37 -0
  36. package/lib/langium/trivia/trivia-contribution.d.ts.map +1 -0
  37. package/lib/langium/trivia/trivia-contribution.js +10 -0
  38. package/lib/langium/trivia/trivia-contribution.js.map +1 -0
  39. package/lib/langium/trivia/trivia-preserver.d.ts +50 -0
  40. package/lib/langium/trivia/trivia-preserver.d.ts.map +1 -0
  41. package/lib/langium/trivia/trivia-preserver.js +10 -0
  42. package/lib/langium/trivia/trivia-preserver.js.map +1 -0
  43. package/lib/langium/trivia/trivia-service.d.ts +70 -0
  44. package/lib/langium/trivia/trivia-service.d.ts.map +1 -0
  45. package/lib/langium/trivia/trivia-service.js +56 -0
  46. package/lib/langium/trivia/trivia-service.js.map +1 -0
  47. package/lib/testing/node/scratch-workspace.js +2 -2
  48. package/package.json +5 -5
  49. package/src/index.ts +1 -0
  50. package/src/langium/integrity/integrity-service.ts +9 -1
  51. package/src/langium/language-module.ts +38 -0
  52. package/src/langium/model-service/model-service.ts +58 -19
  53. package/src/langium/residency/cst-residency-service.ts +4 -6
  54. package/src/langium/serialization/abstract-serializer.ts +13 -32
  55. package/src/langium/trivia/comment-preserver.ts +1100 -0
  56. package/src/langium/trivia/document-ending-preserver.ts +55 -0
  57. package/src/langium/trivia/index.ts +14 -0
  58. package/src/langium/trivia/trivia-contribution.ts +39 -0
  59. package/src/langium/trivia/trivia-preserver.ts +54 -0
  60. package/src/langium/trivia/trivia-service.ts +99 -0
  61. package/src/testing/node/scratch-workspace.ts +2 -2
@@ -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
  /**