@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.
- package/lib/documents/ast-document-manager.d.ts +19 -13
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +7 -1
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/hydranium-text-documents.d.ts +4 -4
- package/lib/documents/hydranium-text-documents.js +6 -6
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/langium/integrity/integrity-service.js +1 -1
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +58 -15
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +65 -19
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +2 -2
- package/lib/langium/module.d.ts.map +1 -1
- package/lib/langium/naming/name-provider.d.ts +15 -4
- package/lib/langium/naming/name-provider.d.ts.map +1 -1
- package/lib/langium/naming/name-provider.js +2 -1
- package/lib/langium/naming/name-provider.js.map +1 -1
- package/lib/langium/transfer/transfer-encoder.d.ts +9 -9
- package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
- package/lib/langium/transfer/transfer-encoder.js +5 -10
- package/lib/langium/transfer/transfer-encoder.js.map +1 -1
- package/lib/langium/validation/document-validator.d.ts +12 -6
- package/lib/langium/validation/document-validator.d.ts.map +1 -1
- package/lib/langium/validation/document-validator.js +1 -1
- package/lib/langium/validation/document-validator.js.map +1 -1
- package/lib/testing/fake-document.d.ts +2 -1
- package/lib/testing/fake-document.d.ts.map +1 -1
- package/lib/testing/fake-document.js.map +1 -1
- package/lib/testing/make-test-services.d.ts +16 -7
- package/lib/testing/make-test-services.d.ts.map +1 -1
- package/lib/testing/make-test-services.js.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +3 -2
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +2 -1
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-langium-documents.d.ts +3 -2
- package/lib/testing/stub-langium-documents.d.ts.map +1 -1
- package/lib/testing/stub-langium-documents.js.map +1 -1
- package/lib/testing/stub-model-service.d.ts +4 -3
- package/lib/testing/stub-model-service.d.ts.map +1 -1
- package/lib/testing/stub-model-service.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +32 -15
- package/src/documents/hydranium-text-documents.ts +6 -6
- package/src/langium/integrity/integrity-service.ts +1 -1
- package/src/langium/model-service/model-service.ts +81 -23
- package/src/langium/module.ts +2 -2
- package/src/langium/naming/name-provider.ts +17 -5
- package/src/langium/transfer/transfer-encoder.ts +14 -14
- package/src/langium/validation/document-validator.ts +12 -6
- package/src/testing/fake-document.ts +2 -1
- package/src/testing/make-test-services.ts +30 -17
- package/src/testing/stub-ast-document-manager.ts +5 -4
- package/src/testing/stub-langium-documents.ts +3 -2
- 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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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'
|
|
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 (
|
|
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
|
|
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` —
|
|
198
|
-
* `
|
|
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<
|
|
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 =
|
|
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
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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 `
|
|
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.
|
|
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.
|
|
723
|
-
throw new ConflictError(uri, args.
|
|
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
|
}
|
package/src/langium/module.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
258
|
-
* generates `${base}1`,
|
|
259
|
-
* found. Used by create-element
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
111
|
+
document: AstDocument<TAst, AstDiagnostic>
|
|
112
112
|
): TransferDocument<TransferElement, TDiagnostic>;
|
|
113
|
-
toTransferDiagnostic(diagnostic:
|
|
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
|
|
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
|
-
|
|
396
|
-
|
|
394
|
+
return TransferDocument.create(
|
|
395
|
+
source.uri,
|
|
396
|
+
source.version,
|
|
397
397
|
root,
|
|
398
|
-
|
|
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 `
|
|
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,
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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
|
|
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
|
|
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
|
-
):
|
|
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 =
|
|
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
|
|
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<
|
|
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
|
|
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<
|
|
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
|
|
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<
|
|
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
|
|
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,
|
|
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<
|
|
380
|
+
TransferEncoder: TransferEncoder<TTransferDiagnostic>;
|
|
368
381
|
ModelService: ModelService<TAst, TDiagnostic, TTransfer>;
|
|
369
382
|
};
|
|
370
383
|
mutableModel.TransferEncoder = transferEncoder;
|