@hydranium/core 1.0.0-next.79 → 1.0.0-next.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/lib/documents/ast-document-manager.d.ts +19 -13
  2. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  3. package/lib/documents/ast-document-manager.js +7 -1
  4. package/lib/documents/ast-document-manager.js.map +1 -1
  5. package/lib/documents/hydranium-text-documents.d.ts +4 -4
  6. package/lib/documents/hydranium-text-documents.js +6 -6
  7. package/lib/documents/hydranium-text-documents.js.map +1 -1
  8. package/lib/langium/integrity/integrity-service.js +1 -1
  9. package/lib/langium/integrity/integrity-service.js.map +1 -1
  10. package/lib/langium/model-service/model-service.d.ts +58 -15
  11. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  12. package/lib/langium/model-service/model-service.js +65 -19
  13. package/lib/langium/model-service/model-service.js.map +1 -1
  14. package/lib/langium/module.d.ts +2 -2
  15. package/lib/langium/module.d.ts.map +1 -1
  16. package/lib/langium/naming/name-provider.d.ts +15 -4
  17. package/lib/langium/naming/name-provider.d.ts.map +1 -1
  18. package/lib/langium/naming/name-provider.js +2 -1
  19. package/lib/langium/naming/name-provider.js.map +1 -1
  20. package/lib/langium/transfer/transfer-encoder.d.ts +9 -9
  21. package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
  22. package/lib/langium/transfer/transfer-encoder.js +5 -10
  23. package/lib/langium/transfer/transfer-encoder.js.map +1 -1
  24. package/lib/langium/validation/document-validator.d.ts +12 -6
  25. package/lib/langium/validation/document-validator.d.ts.map +1 -1
  26. package/lib/langium/validation/document-validator.js +1 -1
  27. package/lib/langium/validation/document-validator.js.map +1 -1
  28. package/lib/testing/fake-document.d.ts +2 -1
  29. package/lib/testing/fake-document.d.ts.map +1 -1
  30. package/lib/testing/fake-document.js.map +1 -1
  31. package/lib/testing/make-test-services.d.ts +16 -7
  32. package/lib/testing/make-test-services.d.ts.map +1 -1
  33. package/lib/testing/make-test-services.js.map +1 -1
  34. package/lib/testing/stub-ast-document-manager.d.ts +3 -2
  35. package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
  36. package/lib/testing/stub-ast-document-manager.js +2 -1
  37. package/lib/testing/stub-ast-document-manager.js.map +1 -1
  38. package/lib/testing/stub-langium-documents.d.ts +3 -2
  39. package/lib/testing/stub-langium-documents.d.ts.map +1 -1
  40. package/lib/testing/stub-langium-documents.js.map +1 -1
  41. package/lib/testing/stub-model-service.d.ts +4 -3
  42. package/lib/testing/stub-model-service.d.ts.map +1 -1
  43. package/lib/testing/stub-model-service.js.map +1 -1
  44. package/package.json +5 -5
  45. package/src/documents/ast-document-manager.ts +32 -15
  46. package/src/documents/hydranium-text-documents.ts +6 -6
  47. package/src/langium/integrity/integrity-service.ts +1 -1
  48. package/src/langium/model-service/model-service.ts +81 -23
  49. package/src/langium/module.ts +2 -2
  50. package/src/langium/naming/name-provider.ts +17 -5
  51. package/src/langium/transfer/transfer-encoder.ts +14 -14
  52. package/src/langium/validation/document-validator.ts +12 -6
  53. package/src/testing/fake-document.ts +2 -1
  54. package/src/testing/make-test-services.ts +30 -17
  55. package/src/testing/stub-ast-document-manager.ts +5 -4
  56. package/src/testing/stub-langium-documents.ts +3 -2
  57. package/src/testing/stub-model-service.ts +5 -4
@@ -237,7 +237,7 @@ function contentHash(text: string): string {
237
237
  * per-client staleness guard and never leak into the shared sequence —
238
238
  * the two are different things (an editor's edit-operation counter vs the
239
239
  * document's content-revision number), and splicing them lets versions drift
240
- * silently past `baseVersion` gate holders.
240
+ * silently past based-on gate holders.
241
241
  * - Version-author history so each edit is attributable to its originating client.
242
242
  * - Pending-content staging used by the integrity service to thread corrections
243
243
  * through `workspace/applyEdit` cycles for currently-closed documents.
@@ -264,7 +264,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
264
264
  * that record is deleted on last close, while the version sequence must
265
265
  * survive it — the shared version is a server-owned, monotonic,
266
266
  * advances-iff-content-changes counter that never resets while the server
267
- * lives. That invariant is what makes an optimistic `baseVersion` gate
267
+ * lives. That invariant is what makes an optimistic based-on gate
268
268
  * sound: "version unchanged ⇔ content unchanged", with no false conflicts
269
269
  * from close/reopen version resets and no false passes from a reopened
270
270
  * sequence coincidentally landing on a stale writer's number.
@@ -506,7 +506,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
506
506
  }
507
507
 
508
508
  // The SHARED version advances iff the content actually changes — the
509
- // invariant optimistic `baseVersion` gates rely on. The new text is
509
+ // invariant optimistic based-on gates rely on. The new text is
510
510
  // only known after applying the (possibly incremental) changes, so
511
511
  // apply at a tentative +1 and roll the version back on an identical
512
512
  // result (an empty-changes update only re-stamps the version).
@@ -557,7 +557,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
557
557
  * A content-identical write still fires the change event (rebuild): the
558
558
  * authored write's server-side rebuild is correctness-bearing, it just
559
559
  * mints no new version — nothing observable changed, so watchers'
560
- * `baseVersion` pointers stay valid.
560
+ * based-on versions stay valid.
561
561
  *
562
562
  * Returns the resulting shared version. Throws when the document is not
563
563
  * open — callers (`AstDocumentManager.update`) open first.
@@ -694,7 +694,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
694
694
  const source = pendingText ? ', source=pending' : '';
695
695
  // The SHARED version is server-assigned: continue the persisted
696
696
  // sequence — same version when the content is unchanged since the
697
- // last close (so watchers' `baseVersion` pointers stay valid), one
697
+ // last close (so watchers' based-on versions stay valid), one
698
698
  // step when it changed (so no stale pointer can coincidentally pass
699
699
  // the optimistic gate). Only a first-ever open adopts the client's
700
700
  // declared id as the sequence seed.
@@ -860,7 +860,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
860
860
  * `0` for a URI this store has never seen. The shared sequence is
861
861
  * server-owned and monotonic across close/reopen cycles, and advances
862
862
  * exactly when the synced content changes — which is what makes it a sound
863
- * optimistic-concurrency token (`baseVersion` gates): version unchanged ⇔
863
+ * optimistic-concurrency token (based-on gates): version unchanged ⇔
864
864
  * content unchanged.
865
865
  */
866
866
  version(uri: DocumentUri): number {
@@ -360,7 +360,7 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
360
360
  // during the re-parse saw the text on disk, which in `'editor'` sync mode
361
361
  // is the PRE-repair text. Leave the sequence describing that and the next
362
362
  // open hashes the repair, finds a mismatch and steps the version again, so
363
- // every `baseVersion` taken from this build is stale before it is used.
363
+ // every based-on version taken from this build is stale before it is used.
364
364
  // Falls back to the pre-re-parse number for a URI the store never tracked,
365
365
  // where there is no sequence to advance; an OPEN document answers
366
366
  // `undefined` and keeps the store's own version, which is not this
@@ -11,12 +11,12 @@ import {
11
11
  type CanonicalUri,
12
12
  type CloseModelArgs,
13
13
  ConflictError,
14
+ isSnapshotVersion,
14
15
  defineMessage,
15
16
  Logger,
16
17
  type MaybeObservableValue,
17
18
  type MaybePromise,
18
19
  ObservableValue,
19
- type TransferDiagnostic,
20
20
  type TransferElement,
21
21
  type OpenModelArgs,
22
22
  type Tracer,
@@ -24,6 +24,7 @@ import {
24
24
  type TransferUpdateArgs
25
25
  } from '@hydranium/protocol';
26
26
  import { type AstNode, DocumentState, type LangiumDocument, UriUtils, type URI } from '@hydranium/langium';
27
+ import { type AstDiagnostic } from '../validation/document-validator.js';
27
28
  import { type DocumentUriPolicy } from '../workspace/document-uri-policy.js';
28
29
  import { ReentrantWriteLockError, isInsideWriteLock } from '../workspace/write-lock-scope.js';
29
30
  import { type CancellationToken, type Disposable } from 'vscode-languageserver';
@@ -194,8 +195,12 @@ export interface ModelServiceOptions extends LogNameOptions {
194
195
  * **Generic parameters.**
195
196
  * - `TAst` — the AST root type each consumer expects on the returned
196
197
  * {@link AstDocument}. Constrained to {@link AstNode}.
197
- * - `TDiagnostic` — wire diagnostic shape used by the injected
198
- * `TransferEncoder`. Defaults to {@link TransferDiagnostic}.
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.
199
204
  * - `TTransfer` — transfer-model root accepted by `update` / `save`
200
205
  * args. Constrained to {@link TransferElement}. Defaults to the
201
206
  * structural base.
@@ -228,7 +233,11 @@ export interface ModelServiceOptions extends LogNameOptions {
228
233
  * member taking a phase as a PARAMETER cannot judge statically and so returns
229
234
  * `TDiagnostic`, leaving the choice to the caller.
230
235
  */
231
- export interface ModelService<TAst extends AstNode, TDiagnostic = TransferDiagnostic, TTransfer extends TransferElement = TransferElement> {
236
+ export interface ModelService<
237
+ TAst extends AstNode,
238
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
239
+ TTransfer extends TransferElement = TransferElement
240
+ > {
232
241
  /**
233
242
  * Resolves once the workspace has been initialised and its first build has
234
243
  * completed — the gate every read should wait behind, since a document
@@ -260,6 +269,7 @@ export interface ModelService<TAst extends AstNode, TDiagnostic = TransferDiagno
260
269
  open(args: OpenModelArgs): Promise<Disposable>;
261
270
  close(args: CloseModelArgs): Promise<void>;
262
271
  isOpen(uri: string): boolean;
272
+ snapshot(uri: string): AstDocument<TAst, TDiagnostic> | undefined;
263
273
  getDocument(uri: string): LangiumDocument | undefined;
264
274
 
265
275
  onModelUpdated(uri: string, listener: (event: AstDocumentUpdatedEvent<TAst, TDiagnostic>) => void): Disposable;
@@ -269,7 +279,7 @@ export interface ModelService<TAst extends AstNode, TDiagnostic = TransferDiagno
269
279
 
270
280
  export class DefaultModelService<
271
281
  TAst extends AstNode,
272
- TDiagnostic = TransferDiagnostic,
282
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
273
283
  /**
274
284
  * Structured payload accepted by `update` / `save`. Constrained to
275
285
  * {@link TransferElement} — the minimal `{ readonly $type: string }`
@@ -578,16 +588,13 @@ export class DefaultModelService<
578
588
  * - `validated` returns `AstDocument<TAst, TDiagnostic>` — diagnostics
579
589
  * are populated.
580
590
  *
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
+ * **The `never` is enforced, not merely declared.** The wait underneath
592
+ * resolves at or ABOVE the requested state, so a document something else
593
+ * already carried past `Validated` would otherwise come back from
594
+ * `settled()` carrying a full diagnostics array typed `never`; these four
595
+ * strip it. An empty array here therefore means "this read does not report
596
+ * diagnostics", never "this document is clean" — call {@link validated}
597
+ * when the answer has to mean the second.
591
598
  *
592
599
  * `settled` is the integrity-overlay name for "all integrity rules
593
600
  * have fired"; it maps to {@link IntegrityService.SettledState} (which
@@ -596,19 +603,19 @@ export class DefaultModelService<
596
603
  * ever moves.
597
604
  */
598
605
  async parsed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
599
- return this.ensureDocumentState(uri, DocumentState.Parsed, cancelToken) as Promise<AstDocument<TAst, never>>;
606
+ return this.withoutDiagnostics(await this.ensureDocumentState(uri, DocumentState.Parsed, cancelToken));
600
607
  }
601
608
 
602
609
  async linked(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
603
- return this.ensureDocumentState(uri, DocumentState.Linked, cancelToken) as Promise<AstDocument<TAst, never>>;
610
+ return this.withoutDiagnostics(await this.ensureDocumentState(uri, DocumentState.Linked, cancelToken));
604
611
  }
605
612
 
606
613
  async settled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
607
- return this.ensureDocumentState(uri, IntegrityService.SettledState, cancelToken) as Promise<AstDocument<TAst, never>>;
614
+ return this.withoutDiagnostics(await this.ensureDocumentState(uri, IntegrityService.SettledState, cancelToken));
608
615
  }
609
616
 
610
617
  async indexed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
611
- return this.ensureDocumentState(uri, DocumentState.IndexedReferences, cancelToken) as Promise<AstDocument<TAst, never>>;
618
+ return this.withoutDiagnostics(await this.ensureDocumentState(uri, DocumentState.IndexedReferences, cancelToken));
612
619
  }
613
620
 
614
621
  async validated(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>> {
@@ -706,7 +713,7 @@ export class DefaultModelService<
706
713
  // no client holds open, the open assigns the shared version from the
707
714
  // INCOMING text, so a version read afterwards has already absorbed the
708
715
  // caller's own write. Gating on it rejected every modifying write to a
709
- // closed document, having compared the caller's `baseVersion` against a
716
+ // closed document, having compared the caller's `basedOn` against a
710
717
  // number the caller itself produced — and a serialised round-trip that is
711
718
  // not byte-identical to the stored text was enough to trigger it. Reading
712
719
  // first keeps both cases the gate exists for: an unknown URI answers 0, so
@@ -716,11 +723,11 @@ export class DefaultModelService<
716
723
  const currentVersion = this.services.workspace.TextDocuments.version(uri);
717
724
  const text = await run('serialize', () => this.modelToText(uri, args.model, cancelToken));
718
725
  await run('open', () => this.open({ uri, clientId: args.clientId, text }));
719
- if (args.baseVersion !== undefined && currentVersion !== args.baseVersion) {
726
+ if (isSnapshotVersion(args.basedOn) && currentVersion !== args.basedOn) {
720
727
  // Distinct from the post-build "superseded" debug line below: this is a
721
728
  // 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);
729
+ this.tracer.debug(`Conflict on ${uri}: based-on v${args.basedOn} stale, server at v${currentVersion}`);
730
+ throw new ConflictError(uri, args.basedOn, currentVersion);
724
731
  }
725
732
  const appliedVersion = await run('apply', () => this.services.workspace.AstDocumentManager.update(uri, text, args.clientId));
726
733
  // Dispatch through the public `rebuild` (which re-canonicalizes the already-
@@ -843,6 +850,31 @@ export class DefaultModelService<
843
850
  return this.services.workspace.AstDocumentManager.isOpen(uri);
844
851
  }
845
852
 
853
+ /**
854
+ * Snapshot of `uri` as it stands RIGHT NOW — the synchronous sibling of the
855
+ * phase reads, which all wait. `undefined` when no document is registered.
856
+ *
857
+ * **This is what a writer wants, and {@link getDocument} is not.** The
858
+ * envelope's `version` is copied by value at projection time, so it cannot
859
+ * move afterwards; a version read off the live document at write time is
860
+ * whatever the server is at *now*, which is the number an optimistic gate is
861
+ * about to compare it against.
862
+ *
863
+ * Diagnostics only from a document that has reached `Validated`, and an
864
+ * empty array otherwise. Unlike the phase reads this one names no phase, so
865
+ * the state it finds is the only thing that can say whether the array
866
+ * describes the content being handed back or whatever an earlier build left.
867
+ * A caller that needs them unconditionally waits, via {@link validated}.
868
+ */
869
+ snapshot(uri: string): AstDocument<TAst, TDiagnostic> | undefined {
870
+ const document = this.getDocument(uri);
871
+ if (!document) {
872
+ return undefined;
873
+ }
874
+ const envelope = AstDocument.from<TAst, TDiagnostic>(document);
875
+ return document.state >= DocumentState.Validated ? envelope : this.withoutDiagnostics(envelope);
876
+ }
877
+
846
878
  /**
847
879
  * The built {@link LangiumDocument} for `uri`, looked up by canonical identity —
848
880
  * the synchronous, phase-agnostic sibling of {@link ensureDocumentState} /
@@ -851,6 +883,13 @@ export class DefaultModelService<
851
883
  * into `LangiumDocuments` directly: a symlinked / `..` / case-divergent URI
852
884
  * still resolves to the one document the build keys by its real path. Returns
853
885
  * `undefined` if no document is registered for `uri`.
886
+ *
887
+ * **Live, so do not take a based-on version off it.** `textDocument` is the
888
+ * store's own object rather than a copy, so `.version` read here answers for
889
+ * the moment of the READ, not the moment of the earlier content — pass it to
890
+ * a write and the server compares its current version against itself, the
891
+ * gate passes unconditionally, and a concurrent edit is overwritten with
892
+ * nothing logged. Use {@link snapshot} for that, or a phase read.
854
893
  */
855
894
  getDocument(uri: string): LangiumDocument | undefined {
856
895
  return this.services.workspace.AstDocumentManager.getDocument(uri);
@@ -1171,4 +1210,23 @@ export class DefaultModelService<
1171
1210
  ? AstDocument.from<TAst, TDiagnostic>(document)
1172
1211
  : AstDocument.create<TAst, TDiagnostic>(uri.toString(), 0, undefined as unknown as TAst, []);
1173
1212
  }
1213
+
1214
+ /**
1215
+ * The same envelope with no diagnostics, for a read that names a phase below
1216
+ * `Validated`.
1217
+ *
1218
+ * Langium fills `LangiumDocument.diagnostics` from inside `validateDocument`
1219
+ * and from nowhere else, so below that phase the array holds whatever an
1220
+ * EARLIER build left — a verdict about text the document may no longer have.
1221
+ * The wait underneath resolves at or above the phase asked for, so a document
1222
+ * something else carried past `Validated` would otherwise hand a full array
1223
+ * back from `parsed()`.
1224
+ *
1225
+ * A copy rather than a clear: the envelope is freshly built here, but
1226
+ * {@link toAstDocument} is overridable and an adopter's version may return
1227
+ * one it also keeps.
1228
+ */
1229
+ protected withoutDiagnostics(document: AstDocument<TAst, TDiagnostic>): AstDocument<TAst, never> {
1230
+ return { ...document, diagnostics: [] };
1231
+ }
1174
1232
  }
@@ -216,7 +216,7 @@ export interface ServerAddedSharedServices<TProject extends Project = Project> {
216
216
  * that need to narrow the diagnostic type or override lifecycle
217
217
  * behaviour rebind this slot with a subclass.
218
218
  */
219
- AstDocumentManager: AstDocumentManager<AstNode, unknown>;
219
+ AstDocumentManager: AstDocumentManager<AstNode>;
220
220
  /**
221
221
  * Wires the framework's build-time features (integrity, AST
222
222
  * enrichment) into Langium's build pipeline: owns the build-phase
@@ -272,7 +272,7 @@ export interface ServerAddedSharedServices<TProject extends Project = Project> {
272
272
  */
273
273
  model: {
274
274
  TransferEncoder: TransferEncoder;
275
- ModelService: ModelService<AstNode, unknown>;
275
+ ModelService: ModelService<AstNode>;
276
276
  };
277
277
  /**
278
278
  * Shared contribution group for batch-level build-phase passes (see
@@ -254,10 +254,21 @@ export interface NameProvider extends LangiumNameProvider {
254
254
  /**
255
255
  * Find a unique name for a new node within `container`, using `base`
256
256
  * as a stem. Walks the AST subtree rooted at `container` and avoids
257
- * names already used by `$type === type` nodes there. Default
258
- * generates `${base}1`, `${base}2`, ... until an unused value is
259
- * found. Used by create-element operation handlers when the
260
- * uniqueness scope is one AST subtree.
257
+ * names already held there by nodes of `type` or of any subtype of it,
258
+ * as `AstReflection.isSubtype` reports. Default generates `${base}1`,
259
+ * `${base}2`, ... until an unused value is found. Used by create-element
260
+ * operation handlers when the uniqueness scope is one AST subtree.
261
+ *
262
+ * Subtype-closed rather than `$type`-exact, because a name space shared by
263
+ * several concrete types is named by their common supertype and by no
264
+ * concrete type at all — the shape a cross-reference whose target type is
265
+ * that supertype produces. An exact filter finds no collisions for such a
266
+ * call and returns the proposal unchanged, which a caller cannot tell from
267
+ * a name that was genuinely free.
268
+ * {@link NameProvider.findNextDocumentQualifiedName} and
269
+ * {@link NameProvider.findNextProjectQualifiedName} resolve `type` the same
270
+ * way, through `IndexManager.allElements`, so one type argument does not
271
+ * mean different sets at different tiers.
261
272
  */
262
273
  findNextName(type: string, base: string, container: AstNode): string;
263
274
 
@@ -496,8 +507,9 @@ export class DefaultNameProvider implements NameProvider {
496
507
 
497
508
  findNextName(type: string, base: string, container: AstNode): string {
498
509
  const proposal = base.replaceAll(this.nameSeparator, '_');
510
+ const reflection = this.services.shared.AstReflection;
499
511
  const knownNames = AstUtils.streamAst(container)
500
- .filter(node => node.$type === type)
512
+ .filter(node => reflection.isSubtype(node.$type, type))
501
513
  .map(node => this.getOwnName(node))
502
514
  .nonNullable()
503
515
  .toArray();
@@ -12,7 +12,7 @@ import {
12
12
  type Tracer,
13
13
  type TransferDiagnostic,
14
14
  type TransferElement,
15
- type TransferDocument,
15
+ TransferDocument,
16
16
  type TransferTypeFor
17
17
  } from '@hydranium/protocol';
18
18
  import { AstUtils, type AstNode, DocumentCache, DocumentState, isAstNode, isReference, type LangiumDocument } from '@hydranium/langium';
@@ -20,7 +20,7 @@ import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-protocol';
20
20
  import { type AstDocument } from '../../documents/ast-document-manager.js';
21
21
  import { type LogNameOptions } from '../diagnostics/logger.js';
22
22
  import { type ServerSharedServicesMinimal } from '../shared-services.js';
23
- import { type TransferLspDiagnostic } from '../validation/document-validator.js';
23
+ import { type AstDiagnostic } from '../validation/document-validator.js';
24
24
 
25
25
  /**
26
26
  * Which set of properties {@link TransferEncoder.toTransfer} emits.
@@ -108,9 +108,9 @@ export interface TransferEncoder<TDiagnostic extends TransferDiagnostic = Transf
108
108
  toTransfer<T extends AstNode>(ast: T, mode?: TransferMode): TransferElement;
109
109
  toTransferDocument(langiumDocument: LangiumDocument): TransferDocument<TransferElement, TDiagnostic>;
110
110
  astDocumentToTransferDocument<TAst extends AstNode>(
111
- document: AstDocument<TAst, TDiagnostic | TransferLspDiagnostic>
111
+ document: AstDocument<TAst, AstDiagnostic>
112
112
  ): TransferDocument<TransferElement, TDiagnostic>;
113
- toTransferDiagnostic(diagnostic: TransferLspDiagnostic): TDiagnostic;
113
+ toTransferDiagnostic(diagnostic: AstDiagnostic): TDiagnostic;
114
114
  }
115
115
 
116
116
  /**
@@ -155,7 +155,7 @@ export interface TransferEncoder<TDiagnostic extends TransferDiagnostic = Transf
155
155
  * - {@link toTransferDocument} — bundle root + diagnostics into the wire
156
156
  * {@link TransferDocument} envelope. Override to filter / merge
157
157
  * diagnostics from auxiliary sources or to project a synthesised root.
158
- * - {@link toTransferDiagnostic} — project a {@link TransferLspDiagnostic}
158
+ * - {@link toTransferDiagnostic} — project a {@link AstDiagnostic}
159
159
  * (the framework validator's output) to {@link TDiagnostic}.
160
160
  */
161
161
  export class DefaultTransferEncoder<
@@ -391,12 +391,12 @@ export class DefaultTransferEncoder<
391
391
  source: TransferEnvelopeSource<TDiagnostic>,
392
392
  root: TransferTypeFor<TAst, TTransferMap>
393
393
  ): TransferDocument<TransferTypeFor<TAst, TTransferMap>, TDiagnostic> {
394
- return {
395
- uri: source.uri,
396
- version: source.version,
394
+ return TransferDocument.create(
395
+ source.uri,
396
+ source.version,
397
397
  root,
398
- diagnostics: source.diagnostics.map(diagnostic => this.toTransferDiagnostic(diagnostic as TransferLspDiagnostic))
399
- };
398
+ source.diagnostics.map(diagnostic => this.toTransferDiagnostic(diagnostic as AstDiagnostic))
399
+ );
400
400
  }
401
401
 
402
402
  /**
@@ -410,7 +410,7 @@ export class DefaultTransferEncoder<
410
410
  * second `LangiumDocuments.getDocument` lookup.
411
411
  *
412
412
  * The `diagnostic` projection assumes the snapshot's diagnostics carry
413
- * the `TransferLspDiagnostic` shape ({@link Diagnostic} plus the
413
+ * the `AstDiagnostic` shape ({@link Diagnostic} plus the
414
414
  * framework validator's `element` / `property` decoration). Snapshots
415
415
  * built from raw Langium diagnostics structurally satisfy this — the
416
416
  * extra fields are optional in the projection.
@@ -421,7 +421,7 @@ export class DefaultTransferEncoder<
421
421
  * assembly seam rather than duplicating it.
422
422
  */
423
423
  astDocumentToTransferDocument<TAst extends AstNode>(
424
- document: AstDocument<TAst, TDiagnostic | TransferLspDiagnostic>
424
+ document: AstDocument<TAst, AstDiagnostic>
425
425
  ): TransferDocument<TransferTypeFor<TAst, TTransferMap>, TDiagnostic> {
426
426
  const root = this.encodeNode(document.root, this.createEncodeContext('full', document.uri)) as TransferTypeFor<TAst, TTransferMap>;
427
427
  return this.assembleTransferDocument<TAst>(document, root);
@@ -429,7 +429,7 @@ export class DefaultTransferEncoder<
429
429
 
430
430
  /**
431
431
  * Project an LSP-shaped diagnostic (typically a
432
- * {@link TransferLspDiagnostic} produced by
432
+ * {@link AstDiagnostic} produced by
433
433
  * `HydraniumDocumentValidator` — adds `element` + optional `property`
434
434
  * to the standard {@link Diagnostic} shape) to the wire-diagnostic
435
435
  * shape.
@@ -448,7 +448,7 @@ export class DefaultTransferEncoder<
448
448
  * Override only if the wire-diagnostic shape needs fields beyond
449
449
  * what {@link TransferDiagnostic} carries.
450
450
  */
451
- toTransferDiagnostic(diagnostic: TransferLspDiagnostic): TDiagnostic {
451
+ toTransferDiagnostic(diagnostic: AstDiagnostic): TDiagnostic {
452
452
  const langiumCode = (diagnostic.data as { code?: string } | undefined)?.code;
453
453
  // The same `data` the Langium code comes out of also carries the framework
454
454
  // message identity, under its own key so the two conventions co-exist.
@@ -46,9 +46,15 @@ import { isSyntheticNode } from '../workspace/synthetic.js';
46
46
  import { isVirtualUri } from '../workspace/virtual-document.js';
47
47
 
48
48
  /**
49
- * Diagnostic read off a `LangiumDocument`: an LSP {@link Diagnostic} that MAY
50
- * carry the protocol-level `element` path and `property` name from
51
- * {@link TransferDiagnostic}.
49
+ * The AST-layer diagnostic: what the build left on `LangiumDocument`, and what
50
+ * an `AstDocument` carries. An LSP {@link Diagnostic} that MAY also carry the
51
+ * `element` path and `property` name from {@link TransferDiagnostic}.
52
+ *
53
+ * **Not interchangeable with {@link TransferDiagnostic}, which is the other end
54
+ * of the same conversion.** They disagree on field types as well as on which
55
+ * fields exist — `severity` is LSP's numeric enum here and a string union there
56
+ * — so a slot typed with the wrong one still accepts the value and then reads
57
+ * `undefined`, or compares equal to nothing.
52
58
  *
53
59
  * `element` is optional because a document's diagnostics do not all come from
54
60
  * this validator. {@link HydraniumDocumentValidator.toDiagnostic} always sets
@@ -63,7 +69,7 @@ import { isVirtualUri } from '../workspace/virtual-document.js';
63
69
  * condition. Consumers therefore cast at the read, and the cast is sound only
64
70
  * because this field is optional.
65
71
  */
66
- export interface TransferLspDiagnostic extends Diagnostic {
72
+ export interface AstDiagnostic extends Diagnostic {
67
73
  element?: string;
68
74
  property?: string;
69
75
  }
@@ -277,7 +283,7 @@ export interface DocumentValidatorOptions extends LogNameOptions {
277
283
  * - **Skip-validation hook** via the virtual {@link shouldSkipValidation}
278
284
  * predicate, which adopters override to compose additional skip reasons.
279
285
  * - **Diagnostic mapping** to {@link TransferDiagnostic}-shaped
280
- * {@link TransferLspDiagnostic}: every emitted diagnostic carries the AST
286
+ * {@link AstDiagnostic}: every emitted diagnostic carries the AST
281
287
  * node's path (built via Langium's `AstNodeLocator`) plus the optional
282
288
  * offending property name.
283
289
  * - **Optional timing wrap** on `validateDocument`, on by default.
@@ -668,7 +674,7 @@ export class HydraniumDocumentValidator extends DefaultDocumentValidator {
668
674
  severity: ValidationSeverity,
669
675
  message: string,
670
676
  info: DiagnosticInfo<N, string>
671
- ): TransferLspDiagnostic {
677
+ ): AstDiagnostic {
672
678
  const base = super.toDiagnostic(severity, message, info);
673
679
  const node = info.node;
674
680
  if (!node) {
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { DocumentState, type AstNode, type AstReflection, type LangiumDocument, UriUtils, type URI } from '@hydranium/langium';
11
11
  import { buildAstNode } from '../langium/ast-extension/ast-node-builder.js';
12
+ import { type AstDiagnostic } from '../langium/validation/document-validator.js';
12
13
 
13
14
  /**
14
15
  * Build a typed AST-node fixture for unit tests.
@@ -104,7 +105,7 @@ function stringifyFakeRoot(root: unknown): string {
104
105
  * staging. Override via `options.state` for tests that gate on earlier
105
106
  * phases.
106
107
  */
107
- export function makeFakeDocument<TAst extends AstNode = AstNode, TDiagnostic = unknown>(
108
+ export function makeFakeDocument<TAst extends AstNode = AstNode, TDiagnostic extends AstDiagnostic = AstDiagnostic>(
108
109
  uri: string | URI,
109
110
  root: TAst,
110
111
  options: FakeDocumentOptions<TAst, TDiagnostic> = {}
@@ -24,6 +24,7 @@ import type { Harness } from '@hydranium/protocol/testing';
24
24
  import { type AstNode, type AstNodeDescription, type WorkspaceLock } from '@hydranium/langium';
25
25
  import type { ModelService, ModelServiceOptions } from '../langium/model-service/model-service.js';
26
26
  import type { ServerSharedServices } from '../langium/module.js';
27
+ import type { AstDiagnostic } from '../langium/validation/document-validator.js';
27
28
  import { DefaultTransferEncoder, type TransferEncoder } from '../langium/transfer/transfer-encoder.js';
28
29
  import { DefaultDocumentUriPolicy, type DocumentUriPolicy } from '../langium/workspace/document-uri-policy.js';
29
30
  import { HydraniumWorkspaceLock } from '../langium/workspace/hydranium-workspace-lock.js';
@@ -48,12 +49,21 @@ import { makeStubWritableFileSystem, type StubWritableFileSystem } from './stub-
48
49
  * to `ServerSharedServices` in {@link makeTestServices} is therefore the
49
50
  * single named place where the "stub stands in for the full service tree"
50
51
  * assertion happens.
52
+ *
53
+ * **`TDiagnostic` and `TTransferDiagnostic` are the two ends of a conversion
54
+ * and must stay separate.** The first is what the build left on
55
+ * `LangiumDocument.diagnostics` and what `ModelService` hands back on an
56
+ * `AstDocument`; the second is what `TransferEncoder.toTransferDiagnostic`
57
+ * produces for the wire. One parameter serving both makes the tree
58
+ * unbindable for an adopter who extends either shape, since extending one
59
+ * says nothing about the other.
51
60
  */
52
61
  export interface TestSharedServices<
53
62
  TAst extends AstNode = AstNode,
54
- TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
63
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
55
64
  TTransfer extends TransferElement = TransferElement,
56
- TProject extends Project = Project
65
+ TProject extends Project = Project,
66
+ TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
57
67
  > {
58
68
  readonly Clock: Clock;
59
69
  readonly Logger: Logger;
@@ -92,7 +102,7 @@ export interface TestSharedServices<
92
102
  WorkspaceLock: WorkspaceLock;
93
103
  };
94
104
  readonly model: {
95
- TransferEncoder: TransferEncoder<TDiagnostic>;
105
+ TransferEncoder: TransferEncoder<TTransferDiagnostic>;
96
106
  ModelService: ModelService<TAst, TDiagnostic, TTransfer>;
97
107
  };
98
108
  /**
@@ -109,9 +119,10 @@ export interface TestSharedServices<
109
119
  /** Optional configuration for {@link makeTestServices}. */
110
120
  export interface MakeTestServicesOptions<
111
121
  TAst extends AstNode = AstNode,
112
- TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
122
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
113
123
  TTransfer extends TransferElement = TransferElement,
114
- TProject extends Project = Project
124
+ TProject extends Project = Project,
125
+ TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
115
126
  > {
116
127
  /**
117
128
  * Serialiser used by the bundled `StubModelService`. The stub tree
@@ -206,7 +217,7 @@ export interface MakeTestServicesOptions<
206
217
  * Override the {@link TransferEncoder} factory. Default: framework
207
218
  * {@link TransferEncoder} with no overrides.
208
219
  */
209
- transferEncoder?: (services: ServerSharedServices<TProject>) => TransferEncoder<TDiagnostic>;
220
+ transferEncoder?: (services: ServerSharedServices<TProject>) => TransferEncoder<TTransferDiagnostic>;
210
221
  }
211
222
 
212
223
  /**
@@ -224,9 +235,10 @@ export interface MakeTestServicesOptions<
224
235
  */
225
236
  export interface TestServicesBundle<
226
237
  TAst extends AstNode = AstNode,
227
- TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
238
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
228
239
  TTransfer extends TransferElement = TransferElement,
229
- TProject extends Project = Project
240
+ TProject extends Project = Project,
241
+ TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
230
242
  > extends Harness {
231
243
  readonly services: ServerSharedServices<TProject>;
232
244
  readonly documents: StubLangiumDocuments<TAst, TDiagnostic>;
@@ -248,7 +260,7 @@ export interface TestServicesBundle<
248
260
  */
249
261
  readonly indexManager: StubIndexManager | undefined;
250
262
  readonly modelService: ModelService<TAst, TDiagnostic, TTransfer>;
251
- readonly transferEncoder: TransferEncoder<TDiagnostic>;
263
+ readonly transferEncoder: TransferEncoder<TTransferDiagnostic>;
252
264
  readonly logger: Logger;
253
265
  /** The clock bound on the `Clock` slot — a `makeFakeClock()` if one was passed. */
254
266
  readonly clock: Clock;
@@ -274,12 +286,13 @@ export interface TestServicesBundle<
274
286
  */
275
287
  export function makeTestServices<
276
288
  TAst extends AstNode = AstNode,
277
- TDiagnostic extends TransferDiagnostic = TransferDiagnostic,
289
+ TDiagnostic extends AstDiagnostic = AstDiagnostic,
278
290
  TTransfer extends TransferElement = TransferElement,
279
- TProject extends Project = Project
291
+ TProject extends Project = Project,
292
+ TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
280
293
  >(
281
- options: MakeTestServicesOptions<TAst, TDiagnostic, TTransfer, TProject> = {}
282
- ): TestServicesBundle<TAst, TDiagnostic, TTransfer, TProject> {
294
+ options: MakeTestServicesOptions<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic> = {}
295
+ ): TestServicesBundle<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic> {
283
296
  const documents = makeStubLangiumDocuments<TAst, TDiagnostic>(options.seedDocuments);
284
297
  const textDocuments = makeStubHydraniumTextDocuments();
285
298
  const documentBuilder = makeStubDocumentBuilder();
@@ -316,7 +329,7 @@ export function makeTestServices<
316
329
  // (after the factories run); the literal uses unsafe casts to
317
330
  // partially-built objects to satisfy TestSharedServices, then patches
318
331
  // them in.
319
- const services: TestSharedServices<TAst, TDiagnostic, TTransfer, TProject> = {
332
+ const services: TestSharedServices<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic> = {
320
333
  Clock: clock,
321
334
  Logger: logger,
322
335
  Tracer: tracer,
@@ -338,7 +351,7 @@ export function makeTestServices<
338
351
  ...(indexManager ? { IndexManager: indexManager } : {}),
339
352
  WorkspaceLock: new HydraniumWorkspaceLock()
340
353
  },
341
- model: {} as TestSharedServices<TAst, TDiagnostic, TTransfer, TProject>['model'],
354
+ model: {} as TestSharedServices<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic>['model'],
342
355
  ServerLocale: {} as ServerLocale,
343
356
  MessageRenderer: {} as DefaultMessageRenderer
344
357
  };
@@ -358,13 +371,13 @@ export function makeTestServices<
358
371
  const serialize = options.serialize ?? ((_uri: string, root: TTransfer) => JSON.stringify(root));
359
372
  const transferEncoder = options.transferEncoder
360
373
  ? options.transferEncoder(sharedServices)
361
- : new DefaultTransferEncoder<unknown, TDiagnostic>(sharedServices);
374
+ : new DefaultTransferEncoder<unknown, TTransferDiagnostic>(sharedServices);
362
375
  const modelService = options.modelService
363
376
  ? options.modelService(sharedServices)
364
377
  : makeStubModelService<TAst, TDiagnostic, TTransfer>(sharedServices, serialize, options.modelServiceOptions);
365
378
 
366
379
  const mutableModel = services.model as {
367
- TransferEncoder: TransferEncoder<TDiagnostic>;
380
+ TransferEncoder: TransferEncoder<TTransferDiagnostic>;
368
381
  ModelService: ModelService<TAst, TDiagnostic, TTransfer>;
369
382
  };
370
383
  mutableModel.TransferEncoder = transferEncoder;