@hydranium/core 1.0.0-next.185 → 1.0.0-next.188

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 (96) hide show
  1. package/lib/documents/ast-document-manager.d.ts +26 -44
  2. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  3. package/lib/documents/ast-document-manager.js +20 -50
  4. package/lib/documents/ast-document-manager.js.map +1 -1
  5. package/lib/documents/hydranium-text-documents.d.ts +87 -30
  6. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  7. package/lib/documents/hydranium-text-documents.js +201 -37
  8. package/lib/documents/hydranium-text-documents.js.map +1 -1
  9. package/lib/documents/index.d.ts +2 -0
  10. package/lib/documents/index.d.ts.map +1 -1
  11. package/lib/documents/index.js +2 -0
  12. package/lib/documents/index.js.map +1 -1
  13. package/lib/documents/model-ledger.d.ts +47 -0
  14. package/lib/documents/model-ledger.d.ts.map +1 -0
  15. package/lib/documents/model-ledger.js +30 -0
  16. package/lib/documents/model-ledger.js.map +1 -0
  17. package/lib/documents/version-sync-service.d.ts +134 -0
  18. package/lib/documents/version-sync-service.d.ts.map +1 -0
  19. package/lib/documents/version-sync-service.js +171 -0
  20. package/lib/documents/version-sync-service.js.map +1 -0
  21. package/lib/langium/bootstrap.d.ts.map +1 -1
  22. package/lib/langium/bootstrap.js +16 -0
  23. package/lib/langium/bootstrap.js.map +1 -1
  24. package/lib/langium/document-builder/document-builder.d.ts +31 -25
  25. package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
  26. package/lib/langium/document-builder/document-builder.js +43 -39
  27. package/lib/langium/document-builder/document-builder.js.map +1 -1
  28. package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
  29. package/lib/langium/integrity/integrity-service.js +6 -15
  30. package/lib/langium/integrity/integrity-service.js.map +1 -1
  31. package/lib/langium/model-service/client-session.d.ts +14 -10
  32. package/lib/langium/model-service/client-session.d.ts.map +1 -1
  33. package/lib/langium/model-service/client-session.js +18 -15
  34. package/lib/langium/model-service/client-session.js.map +1 -1
  35. package/lib/langium/model-service/model-service.d.ts +91 -41
  36. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  37. package/lib/langium/model-service/model-service.js +127 -50
  38. package/lib/langium/model-service/model-service.js.map +1 -1
  39. package/lib/langium/module.d.ts +6 -0
  40. package/lib/langium/module.d.ts.map +1 -1
  41. package/lib/langium/module.js +5 -1
  42. package/lib/langium/module.js.map +1 -1
  43. package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
  44. package/lib/langium/residency/cst-residency-service.js +10 -1
  45. package/lib/langium/residency/cst-residency-service.js.map +1 -1
  46. package/lib/langium/shared-services.d.ts +6 -2
  47. package/lib/langium/shared-services.d.ts.map +1 -1
  48. package/lib/langium/shared-services.js.map +1 -1
  49. package/lib/langium/transfer/transfer-encoder.d.ts +19 -10
  50. package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
  51. package/lib/langium/transfer/transfer-encoder.js +8 -7
  52. package/lib/langium/transfer/transfer-encoder.js.map +1 -1
  53. package/lib/langium/workspace/hydranium-langium-document-factory.d.ts +31 -1
  54. package/lib/langium/workspace/hydranium-langium-document-factory.d.ts.map +1 -1
  55. package/lib/langium/workspace/hydranium-langium-document-factory.js +54 -0
  56. package/lib/langium/workspace/hydranium-langium-document-factory.js.map +1 -1
  57. package/lib/langium/workspace/langium-documents.d.ts +7 -0
  58. package/lib/langium/workspace/langium-documents.d.ts.map +1 -1
  59. package/lib/langium/workspace/langium-documents.js +10 -0
  60. package/lib/langium/workspace/langium-documents.js.map +1 -1
  61. package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
  62. package/lib/lsp/hydranium-document-update-handler.js +1 -0
  63. package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
  64. package/lib/testing/make-test-services.d.ts +7 -0
  65. package/lib/testing/make-test-services.d.ts.map +1 -1
  66. package/lib/testing/make-test-services.js +10 -2
  67. package/lib/testing/make-test-services.js.map +1 -1
  68. package/lib/testing/stub-ast-document-manager.d.ts +3 -2
  69. package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
  70. package/lib/testing/stub-ast-document-manager.js +7 -1
  71. package/lib/testing/stub-ast-document-manager.js.map +1 -1
  72. package/lib/testing/stub-hydranium-text-documents.d.ts +2 -2
  73. package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
  74. package/lib/testing/stub-hydranium-text-documents.js +6 -1
  75. package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
  76. package/package.json +5 -5
  77. package/src/documents/ast-document-manager.ts +45 -77
  78. package/src/documents/hydranium-text-documents.ts +225 -46
  79. package/src/documents/index.ts +2 -0
  80. package/src/documents/model-ledger.ts +66 -0
  81. package/src/documents/version-sync-service.ts +267 -0
  82. package/src/langium/bootstrap.ts +16 -0
  83. package/src/langium/document-builder/document-builder.ts +76 -37
  84. package/src/langium/integrity/integrity-service.ts +6 -15
  85. package/src/langium/model-service/client-session.ts +29 -21
  86. package/src/langium/model-service/model-service.ts +182 -64
  87. package/src/langium/module.ts +9 -1
  88. package/src/langium/residency/cst-residency-service.ts +10 -1
  89. package/src/langium/shared-services.ts +6 -2
  90. package/src/langium/transfer/transfer-encoder.ts +25 -18
  91. package/src/langium/workspace/hydranium-langium-document-factory.ts +83 -0
  92. package/src/langium/workspace/langium-documents.ts +11 -0
  93. package/src/lsp/hydranium-document-update-handler.ts +3 -0
  94. package/src/testing/make-test-services.ts +15 -2
  95. package/src/testing/stub-ast-document-manager.ts +10 -2
  96. package/src/testing/stub-hydranium-text-documents.ts +8 -3
@@ -12,6 +12,8 @@ import type { LangiumSharedCoreServices } from '@hydranium/langium';
12
12
  import type { ServerLocale } from '../locale/server-locale.js';
13
13
  import type { MessageRenderer } from '../messages/renderer.js';
14
14
  import type { SelfSaveRegistry } from '../documents/self-save-registry.js';
15
+ import type { ModelLedger } from '../documents/model-ledger.js';
16
+ import type { VersionSyncService } from '../documents/version-sync-service.js';
15
17
  import type { WritableFileSystemProvider } from '../documents/ast-document-manager.js';
16
18
  import type { HydraniumDocumentRegistry } from './workspace/langium-documents.js';
17
19
  import type { BuildPhasePassService } from './build-phase-pass/build-phase-pass-service.js';
@@ -35,8 +37,8 @@ import type { AdditionalDocumentContribution } from './workspace/additional-docu
35
37
  * an {@link ExtendedServiceRegistry}, and on `workspace` a writable file-system
36
38
  * provider plus {@link HydraniumWorkspaceManager}, {@link ProjectManager},
37
39
  * {@link SelfSaveRegistry}, {@link BuildPipelineIntegration},
38
- * {@link BuildPhasePassService}, {@link CstResidencyService} and
39
- * {@link DocumentUriPolicy}.
40
+ * {@link BuildPhasePassService}, {@link CstResidencyService},
41
+ * {@link DocumentUriPolicy}, {@link ModelLedger} and {@link VersionSyncService}.
40
42
  *
41
43
  * Declared structurally (interface extends + intersection on nested slots)
42
44
  * so the full `ServerSharedServices` and any consumer-extended variant
@@ -98,6 +100,8 @@ export interface ServerSharedServicesMinimal<TProject extends Project = Project>
98
100
  BuildPhasePassService: BuildPhasePassService;
99
101
  CstResidencyService: CstResidencyService;
100
102
  DocumentUriPolicy: DocumentUriPolicy;
103
+ ModelLedger: ModelLedger;
104
+ VersionSyncService: VersionSyncService;
101
105
  };
102
106
  }
103
107
 
@@ -8,11 +8,12 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  import {
11
+ asModelVersion,
11
12
  hasMessageIdentity,
13
+ type TransferModelSnapshot,
12
14
  type Tracer,
13
15
  type TransferDiagnostic,
14
16
  type TransferElement,
15
- TransferDocument,
16
17
  type TransferTypeFor
17
18
  } from '@hydranium/protocol';
18
19
  import { AstUtils, type AstNode, DocumentCache, DocumentState, isAstNode, isReference, type LangiumDocument } from '@hydranium/langium';
@@ -75,11 +76,21 @@ export interface EncodeContext {
75
76
  * `diagnostics` accepts either the wire shape (an already-projected
76
77
  * `TDiagnostic`, as a snapshot may carry) or a raw LSP {@link Diagnostic}; the
77
78
  * assembly projects each through {@link TransferEncoder.toTransferDiagnostic}.
79
+ * Absent for a document not validated yet, and left absent on the wire.
78
80
  */
79
81
  export interface TransferEnvelopeSource<TDiagnostic> {
80
82
  readonly uri: string;
81
83
  readonly version: number;
82
- readonly diagnostics: readonly (TDiagnostic | Diagnostic)[];
84
+ readonly diagnostics?: readonly (TDiagnostic | Diagnostic)[];
85
+ }
86
+
87
+ /**
88
+ * A `TransferDocument` as the encoder produces it: the model block without
89
+ * its `hash`, which the data server computes from the encoded model.
90
+ */
91
+ export interface EncodedTransferDocument<TTransfer extends TransferElement, TDiagnostic = TransferDiagnostic> {
92
+ uri: string;
93
+ model: Omit<TransferModelSnapshot<TTransfer, TDiagnostic>, 'hash'>;
83
94
  }
84
95
 
85
96
  /**
@@ -106,10 +117,10 @@ export interface TransferEnvelopeSource<TDiagnostic> {
106
117
  */
107
118
  export interface TransferEncoder<TDiagnostic extends TransferDiagnostic = TransferDiagnostic> {
108
119
  toTransfer<T extends AstNode>(ast: T, mode?: TransferMode): TransferElement;
109
- toTransferDocument(langiumDocument: LangiumDocument): TransferDocument<TransferElement, TDiagnostic>;
120
+ toTransferDocument(langiumDocument: LangiumDocument): EncodedTransferDocument<TransferElement, TDiagnostic>;
110
121
  astDocumentToTransferDocument<TAst extends AstNode>(
111
122
  document: AstDocument<TAst, AstDiagnostic>
112
- ): TransferDocument<TransferElement, TDiagnostic>;
123
+ ): EncodedTransferDocument<TransferElement, TDiagnostic>;
113
124
  toTransferDiagnostic(diagnostic: AstDiagnostic): TDiagnostic;
114
125
  }
115
126
 
@@ -153,7 +164,7 @@ export interface TransferEncoder<TDiagnostic extends TransferDiagnostic = Transf
153
164
  * to special-case reference encoding or to drop specific value shapes.
154
165
  * Envelope-level seams:
155
166
  * - {@link toTransferDocument} — bundle root + diagnostics into the wire
156
- * {@link TransferDocument} envelope. Override to filter / merge
167
+ * {@link EncodedTransferDocument} envelope. Override to filter / merge
157
168
  * diagnostics from auxiliary sources or to project a synthesised root.
158
169
  * - {@link toTransferDiagnostic} — project a {@link AstDiagnostic}
159
170
  * (the framework validator's output) to {@link TDiagnostic}.
@@ -336,7 +347,7 @@ export class DefaultTransferEncoder<
336
347
  }
337
348
 
338
349
  /**
339
- * Build a wire {@link TransferDocument} envelope from a built
350
+ * Build a wire {@link EncodedTransferDocument} envelope from a built
340
351
  * {@link LangiumDocument}. The `'full'` root is served from the internal
341
352
  * {@link rootCache} (memoised per document until it next rebuilds), under
342
353
  * {@link rootCacheKey} — so the data-server's fingerprint and emitted
@@ -360,7 +371,7 @@ export class DefaultTransferEncoder<
360
371
  */
361
372
  toTransferDocument<TAst extends AstNode>(
362
373
  langiumDocument: LangiumDocument
363
- ): TransferDocument<TransferTypeFor<TAst, TTransferMap>, TDiagnostic> {
374
+ ): EncodedTransferDocument<TransferTypeFor<TAst, TTransferMap>, TDiagnostic> {
364
375
  const context = this.createEncodeContext('full', langiumDocument.uri.toString());
365
376
  const root = this.rootCache.get(langiumDocument.uri, this.rootCacheKey(context), () =>
366
377
  this.encodeNode(langiumDocument.parseResult.value, context)
@@ -368,8 +379,8 @@ export class DefaultTransferEncoder<
368
379
  return this.assembleTransferDocument<TAst>(
369
380
  {
370
381
  uri: langiumDocument.textDocument.uri,
371
- version: langiumDocument.textDocument.version,
372
- diagnostics: langiumDocument.diagnostics ?? []
382
+ version: this.services.workspace.ModelLedger.versionOf(langiumDocument.parseResult.value),
383
+ diagnostics: langiumDocument.diagnostics
373
384
  },
374
385
  root
375
386
  );
@@ -390,17 +401,13 @@ export class DefaultTransferEncoder<
390
401
  protected assembleTransferDocument<TAst extends AstNode>(
391
402
  source: TransferEnvelopeSource<TDiagnostic>,
392
403
  root: TransferTypeFor<TAst, TTransferMap>
393
- ): TransferDocument<TransferTypeFor<TAst, TTransferMap>, TDiagnostic> {
394
- return TransferDocument.create(
395
- source.uri,
396
- source.version,
397
- root,
398
- source.diagnostics.map(diagnostic => this.toTransferDiagnostic(diagnostic as AstDiagnostic))
399
- );
404
+ ): EncodedTransferDocument<TransferTypeFor<TAst, TTransferMap>, TDiagnostic> {
405
+ const diagnostics = source.diagnostics?.map(diagnostic => this.toTransferDiagnostic(diagnostic as AstDiagnostic));
406
+ return { uri: source.uri, model: { root, version: asModelVersion(source.version), ...(diagnostics ? { diagnostics } : {}) } };
400
407
  }
401
408
 
402
409
  /**
403
- * Build a wire {@link TransferDocument} envelope from a server-internal
410
+ * Build a wire {@link EncodedTransferDocument} envelope from a server-internal
404
411
  * {@link AstDocument} snapshot (the AST-typed envelope emitted by
405
412
  * `AstDocumentManager.onUpdate` / `onSave` and returned from
406
413
  * `ModelService.request` / `update` / `save`). Distinct from
@@ -422,7 +429,7 @@ export class DefaultTransferEncoder<
422
429
  */
423
430
  astDocumentToTransferDocument<TAst extends AstNode>(
424
431
  document: AstDocument<TAst, AstDiagnostic>
425
- ): TransferDocument<TransferTypeFor<TAst, TTransferMap>, TDiagnostic> {
432
+ ): EncodedTransferDocument<TransferTypeFor<TAst, TTransferMap>, TDiagnostic> {
426
433
  const root = this.encodeNode(document.root, this.createEncodeContext('full', document.uri)) as TransferTypeFor<TAst, TTransferMap>;
427
434
  return this.assembleTransferDocument<TAst>(document, root);
428
435
  }
@@ -12,10 +12,17 @@ import {
12
12
  AstUtils,
13
13
  DefaultLangiumDocumentFactory,
14
14
  type LangiumDocument,
15
+ type Mutable,
15
16
  type ParseResult,
16
17
  type URI
17
18
  } from '@hydranium/langium';
19
+ import { type CancellationToken } from 'vscode-languageserver-protocol';
20
+ import { type TextDocument } from 'vscode-languageserver-textdocument';
21
+ import { STALE_VERSION } from '@hydranium/protocol';
22
+ import { type ModelLedger } from '../../documents/model-ledger.js';
23
+ import { type VersionSyncService } from '../../documents/version-sync-service.js';
18
24
  import { type ExtendedServiceRegistry } from '../service-registry.js';
25
+ import { type ServerSharedServicesMinimal } from '../shared-services.js';
19
26
  import { type Serializer } from '../serialization/serializer.js';
20
27
 
21
28
  /** Structural view of the per-language serializer slot on the resolved services. */
@@ -57,6 +64,16 @@ export class HydraniumLangiumDocumentFactory extends DefaultLangiumDocumentFacto
57
64
  */
58
65
  declare protected readonly serviceRegistry: ExtendedServiceRegistry;
59
66
 
67
+ /** Resolved per call: the sync service reaches the registry, which is built with this factory. */
68
+ protected readonly versionSyncService: () => VersionSyncService;
69
+ protected readonly modelLedger: () => ModelLedger;
70
+
71
+ constructor(services: ServerSharedServicesMinimal) {
72
+ super(services);
73
+ this.versionSyncService = () => services.workspace.VersionSyncService;
74
+ this.modelLedger = () => services.workspace.ModelLedger;
75
+ }
76
+
60
77
  /**
61
78
  * Parse `text` for `uri` under the grammar `languageId` names, instead of
62
79
  * the one `uri` routes to.
@@ -95,6 +112,72 @@ export class HydraniumLangiumDocumentFactory extends DefaultLangiumDocumentFacto
95
112
  return this.createLangiumDocument<T>(parseResult, uri, undefined, text);
96
113
  }
97
114
 
115
+ protected override createLangiumDocument<T extends AstNode = AstNode>(
116
+ parseResult: ParseResult<T>,
117
+ uri: URI,
118
+ textDocument?: TextDocument,
119
+ text?: string
120
+ ): LangiumDocument<T> {
121
+ const document = super.createLangiumDocument(parseResult, uri, textDocument, text);
122
+ // Without `textDocument` Langium creates one at version 0 on first read,
123
+ // which needs a language for `uri` that a folder URI does not have.
124
+ const parsed = parseResult.value.$cstNode?.root.fullText ?? text ?? textDocument?.getText();
125
+ this.versionSyncService().modelProduced(document, { version: this.versionOfParsedText(uri, parsed, textDocument?.version ?? 0) });
126
+ return document;
127
+ }
128
+
129
+ /**
130
+ * The version is read before the parse, for the same reason as in
131
+ * {@link update}: the text document is the store's, which an edit updates
132
+ * in place while the parse runs.
133
+ */
134
+ protected override async createAsync<T extends AstNode = AstNode>(
135
+ uri: URI,
136
+ content: string | TextDocument,
137
+ cancelToken: CancellationToken
138
+ ): Promise<LangiumDocument<T>> {
139
+ const version = typeof content === 'string' ? undefined : content.version;
140
+ const document = await super.createAsync<T>(uri, content, cancelToken);
141
+ if (version !== undefined) {
142
+ this.versionSyncService().modelProduced(document, { version });
143
+ }
144
+ return document;
145
+ }
146
+
147
+ /**
148
+ * The store's version is read before Langium's `update`, which reads the
149
+ * store's text before its first await: read after, it can belong to an
150
+ * edit that arrived while the parse ran.
151
+ *
152
+ * Reconciled with the store here rather than in a `Parsed` listener, which
153
+ * a cancel skips, leaving the root on the version of its earlier text.
154
+ */
155
+ override async update<T extends AstNode = AstNode>(
156
+ document: Mutable<LangiumDocument<T>>,
157
+ cancellationToken: CancellationToken
158
+ ): Promise<LangiumDocument<T>> {
159
+ const stored = this.textDocuments?.get(document.uri.toString())?.version;
160
+ const updated = await super.update(document, cancellationToken);
161
+ const version =
162
+ stored ??
163
+ this.versionOfParsedText(
164
+ updated.uri,
165
+ this.modelLedger().textOf(updated.parseResult.value) ?? updated.textDocument.getText(),
166
+ updated.textDocument.version
167
+ );
168
+ this.versionSyncService().modelProduced(updated, { version });
169
+ return updated;
170
+ }
171
+
172
+ /**
173
+ * `version`, or {@link STALE_VERSION} when the store holds `uri` open with
174
+ * other text than `parsed`: text read from the file while an open landed.
175
+ */
176
+ protected versionOfParsedText(uri: URI, parsed: string | undefined, version: number): number {
177
+ const open = this.textDocuments?.get(uri.toString());
178
+ return open !== undefined && parsed !== undefined && open.getText() !== parsed ? STALE_VERSION : version;
179
+ }
180
+
98
181
  /**
99
182
  * Serialize `model` via the per-language serializer, or `undefined` if none
100
183
  * is bound, it fails, or it is async — `fromModel` is synchronous, so an
@@ -132,6 +132,17 @@ export class HydraniumLangiumDocuments extends DefaultLangiumDocuments implement
132
132
  return this.langiumDocumentFactory.fromString('', uri);
133
133
  }
134
134
 
135
+ /**
136
+ * Registers `document`, then reports it to `VersionSyncService.modelProduced`:
137
+ * a first load parses outside the build, so no build's re-parse reports it.
138
+ * That marks one below `Parsed`, such as the builder's placeholder for a new
139
+ * file, as a placeholder. A rejected duplicate is not reported.
140
+ */
141
+ override addDocument(document: LangiumDocument): void {
142
+ super.addDocument(document);
143
+ this.services.workspace.VersionSyncService.modelProduced(document);
144
+ }
145
+
135
146
  override async getOrCreateDocument(uri: URI): Promise<LangiumDocument<AstNode>> {
136
147
  const resolved = this.uriPolicy.loadUri(uri);
137
148
  if (resolved) {
@@ -171,6 +171,9 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
171
171
  this.bypassNonLanguageClientChanges = options.bypassNonLanguageClientChanges ?? true;
172
172
  this.willSaveGateMs = options.willSaveGateMs ?? 1000;
173
173
  this.textDocuments.onDidSaveInLanguageClient(event => this.editorSaves.get(event.uri)?.());
174
+ services.workspace.VersionSyncService.registerDeferredBuilds(
175
+ uri => this.pendingChanged.has(uri.toString()) || this.pendingDeleted.has(uri.toString())
176
+ );
174
177
  // A change still debounced for a document the store has released would
175
178
  // build it once more after the store's own revert: from the provider
176
179
  // again, or, for a document the revert removed, by reading a file that
@@ -35,6 +35,8 @@ import { DefaultTransferEncoder, type TransferEncoder } from '../langium/transfe
35
35
  import { DefaultDocumentUriPolicy, type DocumentUriPolicy } from '../langium/workspace/document-uri-policy.js';
36
36
  import { HydraniumWorkspaceLock } from '../langium/workspace/hydranium-workspace-lock.js';
37
37
  import { DefaultFileSystemTaskQueue, type FileSystemTaskQueue } from '../documents/file-system-task-queue.js';
38
+ import { DefaultModelLedger, type ModelLedger } from '../documents/model-ledger.js';
39
+ import { DefaultVersionSyncService, type VersionSyncService } from '../documents/version-sync-service.js';
38
40
  import type { FakeDocumentOptions } from './fake-document.js';
39
41
  import { makeStubDocumentBuilder, type StubDocumentBuilder } from './stub-document-builder.js';
40
42
  import { makeStubIndexManager, type StubIndexManager } from './stub-index-manager.js';
@@ -109,6 +111,10 @@ export interface TestSharedServices<
109
111
  * through this tree and let a reentrant write pass unnoticed.
110
112
  */
111
113
  WorkspaceLock: WorkspaceLock;
114
+ /** The REAL ledger: it is a weak map with no dependencies. */
115
+ ModelLedger: ModelLedger;
116
+ /** The REAL service, over this tree's stubs; it reconciles nothing against the stub store. */
117
+ VersionSyncService: VersionSyncService;
112
118
  };
113
119
  readonly model: {
114
120
  TransferEncoder: TransferEncoder<TTransferDiagnostic>;
@@ -266,6 +272,7 @@ export interface TestServicesBundle<
266
272
  readonly selfSaveRegistry: StubSelfSaveRegistry;
267
273
  readonly projectManager: StubProjectManager<TProject>;
268
274
  readonly documentUriPolicy: DocumentUriPolicy;
275
+ readonly modelLedger: ModelLedger;
269
276
  /**
270
277
  * The registry bound on the `ServiceRegistry` slot, or `undefined` when no
271
278
  * {@link MakeTestServicesOptions.languages} were declared.
@@ -320,7 +327,8 @@ export function makeTestServices<
320
327
  const selfSaveRegistry = makeStubSelfSaveRegistry();
321
328
  const fileSystem = makeStubWritableFileSystem(selfSaveRegistry);
322
329
  const projectManager = makeStubProjectManager<TProject>(options.seedProjects);
323
- const astDocumentManager = makeStubAstDocumentManager<TAst, TDiagnostic>(textDocuments, fileSystem, documents);
330
+ const modelLedger = new DefaultModelLedger();
331
+ const astDocumentManager = makeStubAstDocumentManager<TAst, TDiagnostic>(textDocuments, fileSystem, documents, modelLedger);
324
332
  const documentUriPolicy = options.documentUriPolicy ?? new DefaultDocumentUriPolicy();
325
333
  const serviceRegistry = options.languages
326
334
  ? makeStubServiceRegistry(options.languages, { openLanguageIds: options.openLanguageIds })
@@ -368,7 +376,10 @@ export function makeTestServices<
368
376
  // be genuinely ABSENT, not present-and-undefined, when no index was seeded.
369
377
  ...(indexManager ? { IndexManager: indexManager } : {}),
370
378
  WorkspaceLock: workspaceLock,
371
- FileSystemTaskQueue: new DefaultFileSystemTaskQueue({ workspace: { DocumentUriPolicy: documentUriPolicy } })
379
+ FileSystemTaskQueue: new DefaultFileSystemTaskQueue({ workspace: { DocumentUriPolicy: documentUriPolicy } }),
380
+ ModelLedger: modelLedger,
381
+ // Patched in below: it reads the tree it belongs to.
382
+ VersionSyncService: {} as VersionSyncService
372
383
  },
373
384
  model: {} as TestSharedServices<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic>['model'],
374
385
  ServerLocale: {} as ServerLocale,
@@ -378,6 +389,7 @@ export function makeTestServices<
378
389
 
379
390
  // Patched in after the literal, like `model` below: both read the tree they
380
391
  // belong to, and the renderer reads the locale service.
392
+ services.workspace.VersionSyncService = new DefaultVersionSyncService(sharedServices);
381
393
  const mutableMessages = services as { ServerLocale: ServerLocale; MessageRenderer: MessageRenderer };
382
394
  const serverLocale = new DefaultServerLocale(sharedServices);
383
395
  if (options.locale) {
@@ -414,6 +426,7 @@ export function makeTestServices<
414
426
  selfSaveRegistry,
415
427
  projectManager,
416
428
  documentUriPolicy,
429
+ modelLedger,
417
430
  serviceRegistry,
418
431
  indexManager,
419
432
  modelService,
@@ -18,7 +18,8 @@ import {
18
18
  type WritableFileSystemProvider
19
19
  } from '../documents/ast-document-manager.js';
20
20
  import { UNKNOWN_CLIENT_ID } from '../documents/client-ids.js';
21
- import type { CloseModelArgs, OpenModelArgs } from '@hydranium/protocol';
21
+ import { DefaultModelLedger, type ModelLedger } from '../documents/model-ledger.js';
22
+ import { type CloseModelArgs, type OpenModelArgs } from '@hydranium/protocol';
22
23
  import type { StubHydraniumTextDocuments } from './stub-hydranium-text-documents.js';
23
24
  import type { StubLangiumDocuments } from './stub-langium-documents.js';
24
25
 
@@ -60,6 +61,7 @@ export interface StubAstDocumentManager<TAst extends AstNode, TDiagnostic extend
60
61
  | 'onClientClosed'
61
62
  | 'getAuthor'
62
63
  | 'getDocument'
64
+ | 'toAstDocument'
63
65
  | 'attributeUpdate'
64
66
  > {
65
67
  readonly openClients: Map<string, Set<string>>;
@@ -109,7 +111,8 @@ export interface StubAstDocumentManager<TAst extends AstNode, TDiagnostic extend
109
111
  export function makeStubAstDocumentManager<TAst extends AstNode, TDiagnostic extends AstDiagnostic = AstDiagnostic>(
110
112
  textDocuments: StubHydraniumTextDocuments,
111
113
  fileSystem: Pick<WritableFileSystemProvider, 'writeFile'>,
112
- documents?: StubLangiumDocuments<TAst, TDiagnostic>
114
+ documents?: StubLangiumDocuments<TAst, TDiagnostic>,
115
+ ledger: ModelLedger = new DefaultModelLedger()
113
116
  ): StubAstDocumentManager<TAst, TDiagnostic> {
114
117
  const openClients = new Map<string, Set<string>>();
115
118
  // Keyed by the URI as given, not canonicalized: the stub has no URI policy,
@@ -226,6 +229,11 @@ export function makeStubAstDocumentManager<TAst extends AstNode, TDiagnostic ext
226
229
  // that need real symlink canonicalization wire the real AstDocumentManager).
227
230
  getDocument(uri: string): LangiumDocument | undefined {
228
231
  return documents?.getDocument(UriUtils.toUri(uri));
232
+ },
233
+ toAstDocument(document: LangiumDocument): AstDocument<TAst, TDiagnostic> {
234
+ const uri = document.textDocument.uri;
235
+ const root = document.parseResult.value as TAst;
236
+ return AstDocument.create<TAst, TDiagnostic>(uri, ledger.versionOf(root), root, document.diagnostics as TDiagnostic[] | undefined);
229
237
  }
230
238
  };
231
239
  }
@@ -7,7 +7,7 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import type { CanonicalUri } from '@hydranium/protocol';
10
+ import { type CanonicalUri, textHash } from '@hydranium/protocol';
11
11
  import type { ApplyWorkspaceEditResult } from 'vscode-languageserver';
12
12
  import { Disposable, Emitter } from 'vscode-languageserver';
13
13
  import type { TextDocument } from 'vscode-languageserver-textdocument';
@@ -36,7 +36,7 @@ export interface StubTextDocumentEntry {
36
36
  * `isOpenInAnyClient` / `isOpenInClient` / `isRevertPending` /
37
37
  * `openDocuments`), the push channel
38
38
  * to the language client (`applyEditToLanguageClient` / `stagePendingContent`),
39
- * the save / close notifications, the dirty state (`isDirty` /
39
+ * the save / close notifications, the dirty state (`isDirty` / `textState` /
40
40
  * `onDidChangeDirty` / `updateDiskBaseline`, against a baseline {@link seedOpen}
41
41
  * sets and a save that carries its text moves), and the client-session table
42
42
  * (`registerSession` / `closeSession` / `onDidCloseSession`), which is a real
@@ -88,6 +88,7 @@ export interface StubHydraniumTextDocuments extends Pick<
88
88
  | 'onDidCloseSession'
89
89
  | 'onDidCloseLastOpen'
90
90
  | 'isDirty'
91
+ | 'textState'
91
92
  | 'onDidChangeDirty'
92
93
  | 'updateDiskBaseline'
93
94
  > {
@@ -176,7 +177,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
176
177
  } else {
177
178
  dirty.delete(uri);
178
179
  }
179
- dirtyChanged.fire({ uri: key(uri), dirty: now });
180
+ dirtyChanged.fire({ uri: key(uri), text: { version: held?.version ?? 0, hash: textHash(held?.text ?? ''), dirty: now } });
180
181
  }
181
182
  };
182
183
 
@@ -200,6 +201,10 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
200
201
  version(uri) {
201
202
  return docs.get(uri)?.version ?? 0;
202
203
  },
204
+ textState(uri) {
205
+ const held = docs.get(uri);
206
+ return held && { version: held.version, hash: textHash(held.text), dirty: dirty.has(uri) };
207
+ },
203
208
  notifyDidChangeTextDocument(event, clientId = LANGUAGE_CLIENT_ID) {
204
209
  const existing = docs.get(event.textDocument.uri);
205
210
  const next: StubTextDocumentEntry = {