@hydranium/core 1.0.0-next.37 → 1.0.0-next.39
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.map +1 -1
- package/lib/documents/ast-document-manager.js +6 -5
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +29 -8
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/language-client-text-shadow.d.ts +24 -1
- package/lib/documents/language-client-text-shadow.d.ts.map +1 -1
- package/lib/documents/language-client-text-shadow.js +36 -2
- package/lib/documents/language-client-text-shadow.js.map +1 -1
- package/lib/langium/integration-services.d.ts +19 -0
- package/lib/langium/integration-services.d.ts.map +1 -1
- package/lib/langium/integration-services.js +7 -4
- package/lib/langium/integration-services.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +5 -0
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +34 -1
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +21 -11
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/testing/stub-document-builder.d.ts +8 -0
- package/lib/testing/stub-document-builder.d.ts.map +1 -1
- package/lib/testing/stub-document-builder.js +22 -3
- package/lib/testing/stub-document-builder.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +6 -5
- package/src/documents/hydranium-text-documents.ts +29 -8
- package/src/documents/language-client-text-shadow.ts +38 -2
- package/src/langium/integration-services.ts +38 -4
- package/src/langium/integrity/integrity-service.ts +35 -1
- package/src/langium/model-service/model-service.ts +21 -11
- package/src/testing/stub-document-builder.ts +32 -3
|
@@ -75,6 +75,11 @@ export namespace IntegrityService {
|
|
|
75
75
|
* at `DocumentState.Validated` and delivered separately via `publishDiagnostics`
|
|
76
76
|
* or the `onModelUpdated` event.
|
|
77
77
|
*
|
|
78
|
+
* The AST is what this guarantees, not the text. For a closed document whose
|
|
79
|
+
* repair was STAGED rather than written (`'editor'` sync mode),
|
|
80
|
+
* `textDocument` still mirrors disk — read the repair off the AST, or from
|
|
81
|
+
* the staged content the next open consumes.
|
|
82
|
+
*
|
|
78
83
|
* Invariant: integrity rules only register at Parsed or Linked, so the build
|
|
79
84
|
* is guaranteed post-integrity once it advances past `onBuildPhase(Linked)`
|
|
80
85
|
* into `IndexedReferences`. {@link IntegrityService.register} enforces this at
|
|
@@ -317,10 +322,11 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
317
322
|
return;
|
|
318
323
|
}
|
|
319
324
|
|
|
325
|
+
const version = document.textDocument.version;
|
|
320
326
|
// Keep the same version so HydraniumTextDocuments doesn't reject subsequent client edits.
|
|
321
327
|
// Use manager.update so the `instanceof FullTextDocument` gate in the bare
|
|
322
328
|
// `TextDocument.update` doesn't trip on adopter-custom text-document types.
|
|
323
|
-
const textDocument = this.textDocuments.update(document.textDocument, [{ text: newText }],
|
|
329
|
+
const textDocument = this.textDocuments.update(document.textDocument, [{ text: newText }], version);
|
|
324
330
|
|
|
325
331
|
await this.syncCorrections(textDocument);
|
|
326
332
|
|
|
@@ -328,6 +334,14 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
328
334
|
// Parsed-phase correction: re-parse only. The build pipeline still runs
|
|
329
335
|
// IndexedContent → ComputedScopes → Linked → … on the fresh AST afterwards,
|
|
330
336
|
// so the document reconciles naturally with no extra work here.
|
|
337
|
+
//
|
|
338
|
+
// Except for a closed document whose repair did not reach disk — `'editor'`
|
|
339
|
+
// sync mode stages it instead of writing it — where the re-parse is SKIPPED:
|
|
340
|
+
// Langium's factory gates the parse on the CST's `fullText`, which still
|
|
341
|
+
// equals the disk text the same call re-reads and redefines `textDocument`
|
|
342
|
+
// over. So `document.textDocument` is NOT post-integrity for such a
|
|
343
|
+
// document — it mirrors disk, and the AST is the only place the repair is
|
|
344
|
+
// legible, until an open consumes the staging.
|
|
331
345
|
await this.documentBuilder.reparse(document, cancelToken);
|
|
332
346
|
} else {
|
|
333
347
|
// Linked-phase (or later) correction. The rule mutated the AST in place but
|
|
@@ -340,6 +354,26 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
340
354
|
// stays in the builder, its proper home, rather than being duplicated here.
|
|
341
355
|
await this.documentBuilder.reparseAndRelink(document, cancelToken);
|
|
342
356
|
}
|
|
357
|
+
|
|
358
|
+
// Re-version against the REPAIRED text, after either branch. The repair is
|
|
359
|
+
// a content change the store has not seen: whatever reconciliation ran
|
|
360
|
+
// during the re-parse saw the text on disk, which in `'editor'` sync mode
|
|
361
|
+
// is the PRE-repair text. Leave the sequence describing that and the next
|
|
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.
|
|
364
|
+
// Falls back to the pre-re-parse number for a URI the store never tracked,
|
|
365
|
+
// where there is no sequence to advance; an OPEN document answers
|
|
366
|
+
// `undefined` and keeps the store's own version, which is not this
|
|
367
|
+
// method's to move.
|
|
368
|
+
//
|
|
369
|
+
// Deliberately NOT gated on cancellation, unlike the entry to this method:
|
|
370
|
+
// `syncCorrections` has already written or staged the repair by now, so a
|
|
371
|
+
// preempted build that skipped this would leave the sequence describing
|
|
372
|
+
// text that is no longer there.
|
|
373
|
+
const reconciled = this.textDocuments.reconcileExternalContent(document.textDocument.uri, newText) ?? version;
|
|
374
|
+
if (document.textDocument.version !== reconciled) {
|
|
375
|
+
this.textDocuments.update(document.textDocument, [], reconciled);
|
|
376
|
+
}
|
|
343
377
|
}
|
|
344
378
|
|
|
345
379
|
/**
|
|
@@ -698,19 +698,29 @@ export class DefaultModelService<
|
|
|
698
698
|
// created from the payload rather than read from the filesystem — `update`
|
|
699
699
|
// is an upsert. For an already-open document `open` refreshes content (the
|
|
700
700
|
// text is ignored on that branch), so existing-document behaviour is
|
|
701
|
-
// unchanged. `version` is intentionally NOT forwarded to `open
|
|
702
|
-
// create stays at its initial version
|
|
703
|
-
//
|
|
701
|
+
// unchanged. `version` is intentionally NOT forwarded to `open`, so a cold
|
|
702
|
+
// create stays at its initial version rather than adopting a number the
|
|
703
|
+
// caller chose.
|
|
704
|
+
//
|
|
705
|
+
// The gate's version is read BEFORE that open, and must be: for a document
|
|
706
|
+
// no client holds open, the open assigns the shared version from the
|
|
707
|
+
// INCOMING text, so a version read afterwards has already absorbed the
|
|
708
|
+
// caller's own write. Gating on it rejected every modifying write to a
|
|
709
|
+
// closed document, having compared the caller's `baseVersion` against a
|
|
710
|
+
// number the caller itself produced — and a serialised round-trip that is
|
|
711
|
+
// not byte-identical to the stored text was enough to trigger it. Reading
|
|
712
|
+
// first keeps both cases the gate exists for: an unknown URI answers 0, so
|
|
713
|
+
// a based-on-version update of a not-yet-existing document still trips it,
|
|
714
|
+
// and a genuine conflict still trips it, another writer having advanced the
|
|
715
|
+
// sequence past the version the caller read.
|
|
716
|
+
const currentVersion = this.services.workspace.TextDocuments.version(uri);
|
|
704
717
|
const text = await run('serialize', () => this.modelToText(uri, args.model, cancelToken));
|
|
705
718
|
await run('open', () => this.open({ uri, clientId: args.clientId, text }));
|
|
706
|
-
if (args.baseVersion !== undefined) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
this.tracer.debug(`Conflict on ${uri}: based-on v${args.baseVersion} stale, server at v${current}`);
|
|
712
|
-
throw new ConflictError(uri, args.baseVersion, current);
|
|
713
|
-
}
|
|
719
|
+
if (args.baseVersion !== undefined && currentVersion !== args.baseVersion) {
|
|
720
|
+
// Distinct from the post-build "superseded" debug line below: this is a
|
|
721
|
+
// 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);
|
|
714
724
|
}
|
|
715
725
|
const appliedVersion = await run('apply', () => this.services.workspace.AstDocumentManager.update(uri, text, args.clientId));
|
|
716
726
|
// Dispatch through the public `rebuild` (which re-canonicalizes the already-
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
type DocumentBuilder,
|
|
12
|
+
type DocumentBuildListener,
|
|
12
13
|
type DocumentPhaseListener,
|
|
13
14
|
type DocumentState,
|
|
14
15
|
type DocumentUpdateListener,
|
|
@@ -94,6 +95,14 @@ export interface StubDocumentBuilder extends Pick<
|
|
|
94
95
|
firePhase(state: DocumentState, document: LangiumDocument, cancelToken?: CancellationToken): void;
|
|
95
96
|
/** Synchronously fire every registered `onUpdate` listener. */
|
|
96
97
|
fireOnUpdate(changed: URI[], deleted: URI[]): void;
|
|
98
|
+
/**
|
|
99
|
+
* Synchronously fire the BUILD-phase listener(s) for `state` with the whole
|
|
100
|
+
* batch. The per-build counterpart of {@link firePhase}: a subject that
|
|
101
|
+
* reports once per build rather than once per document subscribes here, and
|
|
102
|
+
* a stub that only fired {@link firePhase} would leave it silent while
|
|
103
|
+
* looking wired.
|
|
104
|
+
*/
|
|
105
|
+
fireBuildPhase(state: DocumentState, built: LangiumDocument[], cancelToken?: CancellationToken): void;
|
|
97
106
|
/**
|
|
98
107
|
* Hold the next {@link waitUntil} call. The returned handle releases it;
|
|
99
108
|
* the call's return value resolves on the next tick after `resolve` runs.
|
|
@@ -131,6 +140,7 @@ function reraise(result: unknown): void {
|
|
|
131
140
|
*/
|
|
132
141
|
export function makeStubDocumentBuilder(): StubDocumentBuilder {
|
|
133
142
|
const phaseListeners = new Map<DocumentState, DocumentPhaseListener[]>();
|
|
143
|
+
const buildPhaseListeners = new Map<DocumentState, DocumentBuildListener[]>();
|
|
134
144
|
const onUpdateListeners: DocumentUpdateListener[] = [];
|
|
135
145
|
const gates: Array<{ take(release: () => void): void }> = [];
|
|
136
146
|
const updateCalls: RecordedBuilderCall<[URI[], URI[]]>[] = [];
|
|
@@ -194,6 +204,28 @@ export function makeStubDocumentBuilder(): StubDocumentBuilder {
|
|
|
194
204
|
reraise(listener(changed, deleted));
|
|
195
205
|
}
|
|
196
206
|
},
|
|
207
|
+
onBuildPhase(state: DocumentState, listener: DocumentBuildListener) {
|
|
208
|
+
const list = buildPhaseListeners.get(state) ?? [];
|
|
209
|
+
list.push(listener);
|
|
210
|
+
buildPhaseListeners.set(state, list);
|
|
211
|
+
return Disposable.create(() => {
|
|
212
|
+
const idx = list.indexOf(listener);
|
|
213
|
+
if (idx >= 0) {
|
|
214
|
+
list.splice(idx, 1);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
fireBuildPhase(state: DocumentState, built: LangiumDocument[], cancelToken?: CancellationToken) {
|
|
219
|
+
const token =
|
|
220
|
+
cancelToken ??
|
|
221
|
+
({
|
|
222
|
+
isCancellationRequested: false,
|
|
223
|
+
onCancellationRequested: () => Disposable.create(() => undefined)
|
|
224
|
+
} as CancellationToken);
|
|
225
|
+
for (const listener of buildPhaseListeners.get(state) ?? []) {
|
|
226
|
+
reraise(listener(built, token));
|
|
227
|
+
}
|
|
228
|
+
},
|
|
197
229
|
gateNextWaitUntil(): StubWaitUntilGate {
|
|
198
230
|
let release: (() => void) | undefined;
|
|
199
231
|
const entry = {
|
|
@@ -224,9 +256,6 @@ export function makeStubDocumentBuilder(): StubDocumentBuilder {
|
|
|
224
256
|
build() {
|
|
225
257
|
return notSupported('build');
|
|
226
258
|
},
|
|
227
|
-
onBuildPhase() {
|
|
228
|
-
return notSupported('onBuildPhase');
|
|
229
|
-
},
|
|
230
259
|
resetToState() {
|
|
231
260
|
return notSupported('resetToState');
|
|
232
261
|
},
|