@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.
Files changed (33) hide show
  1. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  2. package/lib/documents/ast-document-manager.js +6 -5
  3. package/lib/documents/ast-document-manager.js.map +1 -1
  4. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  5. package/lib/documents/hydranium-text-documents.js +29 -8
  6. package/lib/documents/hydranium-text-documents.js.map +1 -1
  7. package/lib/documents/language-client-text-shadow.d.ts +24 -1
  8. package/lib/documents/language-client-text-shadow.d.ts.map +1 -1
  9. package/lib/documents/language-client-text-shadow.js +36 -2
  10. package/lib/documents/language-client-text-shadow.js.map +1 -1
  11. package/lib/langium/integration-services.d.ts +19 -0
  12. package/lib/langium/integration-services.d.ts.map +1 -1
  13. package/lib/langium/integration-services.js +7 -4
  14. package/lib/langium/integration-services.js.map +1 -1
  15. package/lib/langium/integrity/integrity-service.d.ts +5 -0
  16. package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
  17. package/lib/langium/integrity/integrity-service.js +34 -1
  18. package/lib/langium/integrity/integrity-service.js.map +1 -1
  19. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  20. package/lib/langium/model-service/model-service.js +21 -11
  21. package/lib/langium/model-service/model-service.js.map +1 -1
  22. package/lib/testing/stub-document-builder.d.ts +8 -0
  23. package/lib/testing/stub-document-builder.d.ts.map +1 -1
  24. package/lib/testing/stub-document-builder.js +22 -3
  25. package/lib/testing/stub-document-builder.js.map +1 -1
  26. package/package.json +5 -5
  27. package/src/documents/ast-document-manager.ts +6 -5
  28. package/src/documents/hydranium-text-documents.ts +29 -8
  29. package/src/documents/language-client-text-shadow.ts +38 -2
  30. package/src/langium/integration-services.ts +38 -4
  31. package/src/langium/integrity/integrity-service.ts +35 -1
  32. package/src/langium/model-service/model-service.ts +21 -11
  33. 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 }], document.textDocument.version);
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`: a cold
702
- // create stays at its initial version, so a based-on-`version` update of a
703
- // not-yet-existing document still trips the conflict gate below.
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
- const current = this.services.workspace.TextDocuments.version(uri);
708
- if (current !== args.baseVersion) {
709
- // Distinct from the post-build "superseded" debug line below: this is a
710
- // based-on-stale rejection (the write never applies), not two writes racing.
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
  },