@hydranium/core 1.0.0-next.206 → 1.0.0-next.209

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/document-release-handler.d.ts +6 -1
  2. package/lib/documents/document-release-handler.d.ts.map +1 -1
  3. package/lib/documents/document-release-handler.js +13 -0
  4. package/lib/documents/document-release-handler.js.map +1 -1
  5. package/lib/documents/hydranium-text-documents.d.ts +23 -11
  6. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  7. package/lib/documents/hydranium-text-documents.js +28 -12
  8. package/lib/documents/hydranium-text-documents.js.map +1 -1
  9. package/lib/documents/language-client-shadow.d.ts +19 -13
  10. package/lib/documents/language-client-shadow.d.ts.map +1 -1
  11. package/lib/documents/language-client-shadow.js +16 -13
  12. package/lib/documents/language-client-shadow.js.map +1 -1
  13. package/lib/langium/integrity/integrity-rule.d.ts +6 -6
  14. package/lib/langium/integrity/integrity-service.d.ts +2 -1
  15. package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
  16. package/lib/langium/integrity/integrity-service.js +3 -2
  17. package/lib/langium/integrity/integrity-service.js.map +1 -1
  18. package/lib/langium/model-service/model-service.d.ts +6 -0
  19. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  20. package/lib/langium/model-service/model-service.js +3 -0
  21. package/lib/langium/model-service/model-service.js.map +1 -1
  22. package/lib/testing/stub-hydranium-text-documents.d.ts +9 -4
  23. package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
  24. package/lib/testing/stub-hydranium-text-documents.js +9 -0
  25. package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
  26. package/package.json +5 -5
  27. package/src/documents/document-release-handler.ts +15 -1
  28. package/src/documents/hydranium-text-documents.ts +30 -12
  29. package/src/documents/language-client-shadow.ts +33 -25
  30. package/src/langium/integrity/integrity-rule.ts +6 -6
  31. package/src/langium/integrity/integrity-service.ts +3 -2
  32. package/src/langium/model-service/model-service.ts +9 -0
  33. package/src/testing/stub-hydranium-text-documents.ts +18 -3
@@ -67,7 +67,7 @@ export function isDocumentReleaseSkippedError(error: unknown): error is Document
67
67
  *
68
68
  * The data server announces the first build of a released document as the
69
69
  * release, so a handler that builds nothing leaves that to whichever build
70
- * comes next, a client's edit included.
70
+ * comes next; one that carries a client's write keeps that client's name.
71
71
  *
72
72
  * Extend {@link DefaultDocumentReleaseHandler} to change only the policy: it
73
73
  * keeps the locking and the wait for the build. An implementation written
@@ -142,6 +142,7 @@ export class DefaultDocumentReleaseHandler implements DocumentReleaseHandler {
142
142
  async didReleaseDocument(released: ReleasedDocument): Promise<void> {
143
143
  const target = UriUtils.toUri(released.uri);
144
144
  const outcome = await this.runRelease(released, target);
145
+ this.tracer.with(released.uri).debug(`Release outcome: ${this.formatReleaseOutcome(outcome)}`);
145
146
  if (outcome.kind === 'skipped') {
146
147
  if (!outcome.parsed) {
147
148
  throw new DocumentReleaseSkippedError(outcome.error);
@@ -243,6 +244,19 @@ export class DefaultDocumentReleaseHandler implements DocumentReleaseHandler {
243
244
  }
244
245
  }
245
246
 
247
+ /**
248
+ * `outcome` for the line each release logs at debug: its kind, the
249
+ * decision and whether the document was parsed.
250
+ */
251
+ protected formatReleaseOutcome(outcome: DocumentReleaseOutcome): string {
252
+ if (outcome.kind === 'reclaimed') {
253
+ return 'reclaimed by a client, built nothing';
254
+ }
255
+ const decision = 'decision' in outcome && outcome.decision !== undefined ? ` ${outcome.decision}` : '';
256
+ const parsed = 'parsed' in outcome ? (outcome.parsed ? ', parsed' : ', not parsed') : '';
257
+ return `${outcome.kind}${decision}${parsed}`;
258
+ }
259
+
246
260
  /**
247
261
  * The policy: rebuild a document the file system provider can serve, and
248
262
  * remove any other. Read in the document's disk queue, so it follows any
@@ -203,6 +203,9 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
203
203
  */
204
204
  protected readonly __sessions = new ClientSessionRegistry();
205
205
 
206
+ /** The version each open document was opened at; see {@link openedVersion}. */
207
+ protected readonly __openedVersions = new Map<CanonicalUri, TextVersion>();
208
+
206
209
  protected readonly tracer: Tracer;
207
210
  protected readonly configuration: TextDocumentsConfiguration<T>;
208
211
  protected __textLedger: TextLedger | undefined;
@@ -598,6 +601,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
598
601
  this.textLedger.clearAuthors(uri);
599
602
  const cleanAnnouncement = this.dirtyStateTracker.release(uri);
600
603
  this.__syncedDocuments.delete(uri);
604
+ this.__openedVersions.delete(uri);
601
605
  this.__sessions.forgetClientVersions(uri);
602
606
  // A stage is for a first open; one released unconsumed is stale.
603
607
  this.__pendingContent.delete(uri);
@@ -787,6 +791,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
787
791
  this.log(uri, `Open document: Version ${version} by ${this.formatClientId(clientId)} [first client${source}]`);
788
792
  document = this.create(uri, td.languageId, version, text);
789
793
  this.commitText(uri, document, clientId);
794
+ this.__openedVersions.set(uri, version);
790
795
  // The opener's text, not the staged content: a session's open read
791
796
  // it from the file, and an editor opened its buffer from there. An
792
797
  // editor that opens a buffer it never saved is taken as clean.
@@ -922,6 +927,14 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
922
927
  return this.get(uri)?.version ?? this.textLedger.recordOf(this.documentKey(uri))?.version ?? 0;
923
928
  }
924
929
 
930
+ /**
931
+ * The version the document at `uri` was opened at, which a client's write
932
+ * or an integrity repair steps past; `undefined` while it is not open.
933
+ */
934
+ openedVersion(uri: DocumentUri): TextVersion | undefined {
935
+ return this.__openedVersions.get(this.documentKey(uri));
936
+ }
937
+
925
938
  /**
926
939
  * The text the store holds for `uri`: an open document's, or for a closed
927
940
  * one, the clean text its version sequence left off at, which for a deleted
@@ -1319,17 +1332,16 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
1319
1332
  /**
1320
1333
  * Stages integrity-updated content for a document no client holds.
1321
1334
  *
1322
- * When `workspace/applyEdit` targets a closed file, the client opens it from
1323
- * disk (stale) and sends `didOpen` before applying the edit. This staged
1324
- * content is consumed by {@link notifyDidOpenTextDocument} to replace the
1325
- * stale disk text, preventing a brief revert of the integrity update.
1335
+ * Nothing is pushed to a file no client holds. The next open of it reads
1336
+ * disk, which lacks the update; {@link notifyDidOpenTextDocument} takes this
1337
+ * staged content in place of that text, and the open's sync then delivers
1338
+ * it to an editor as an unsaved change.
1326
1339
  *
1327
1340
  * Only a FIRST open consumes it, so stage only for a URI no client holds
1328
1341
  * ({@link isOpenInAnyClient} is `false`). A URI held only through another
1329
1342
  * head is not closed: an editor attaching to it joins the existing entry and
1330
- * never reads the stage, and the release discards it. An entry lingers
1331
- * only if `workspace/applyEdit` fails and the file is never opened — the
1332
- * memory cost is one serialised string per URI.
1343
+ * never reads the stage, and the release discards it. An entry waits for
1344
+ * that first open, at one serialised string per URI.
1333
1345
  */
1334
1346
  stagePendingContent(uri: DocumentUri, text: string): void {
1335
1347
  this.__pendingContent.set(this.documentKey(uri), text);
@@ -1344,6 +1356,9 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
1344
1356
  * - The shared services have no LSP {@link Connection} bound (non-LSP
1345
1357
  * hosts like CLI / tests).
1346
1358
  * - The shadow already matches `newText` (no edits needed; quiet skip).
1359
+ * - The language client has not opened the document. A client applies an
1360
+ * edit to a closed file by opening it, and that open races the edit;
1361
+ * the document's own open delivers its text instead.
1347
1362
  *
1348
1363
  * On `applyEdit` rejection (`result.applied === false`) or RPC failure the
1349
1364
  * shadow is invalidated so the next call sends a full-replace baseline.
@@ -1381,7 +1396,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
1381
1396
  // under each URI it opened, and each is diffed against its own baseline.
1382
1397
  const key = this.documentKey(uri);
1383
1398
  let lastResult: ApplyWorkspaceEditResult | undefined;
1384
- for (const clientUri of this.languageClientShadow.pushTargets(key, this.toLanguageClientUri(uri))) {
1399
+ for (const clientUri of this.languageClientShadow.pushTargets(key)) {
1385
1400
  // Prepared per target, after the previous target's reply: prepared up
1386
1401
  // front, a later target is diffed against what it held before a change
1387
1402
  // that arrived meanwhile.
@@ -1421,10 +1436,13 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
1421
1436
  }
1422
1437
 
1423
1438
  /**
1424
- * Explicitly baseline the language-client shadow for a URI. Useful in
1425
- * tests and for adopters that need to seed the shadow without going through
1426
- * a `didOpen` event (e.g. after a sideband save). Normal didOpen / didChange
1427
- * paths from the LSP language client already auto-track the shadow.
1439
+ * Explicitly baseline the language-client shadow for a URI the client has
1440
+ * open, for an adopter that knows what the client holds without a
1441
+ * `didChange` saying so (e.g. after a sideband save). Normal didOpen /
1442
+ * didChange paths from the LSP language client already auto-track the
1443
+ * shadow. A seed for a URI the client has not opened is kept but changes no
1444
+ * push, since nothing is pushed there; its open replaces it with the text it
1445
+ * declares.
1428
1446
  */
1429
1447
  setLanguageClientText(uri: DocumentUri, text: string): void {
1430
1448
  this.languageClientShadow.setClientText(this.toLanguageClientUri(uri), text);
@@ -47,10 +47,10 @@ export interface PendingLanguageClientPush {
47
47
  * and applied to the already-advanced synced text the line they insert
48
48
  * lands twice.
49
49
  *
50
- * `undefined` only when that buffer is unknown — the client never declared
51
- * one, or a rejection invalidated what was tracked. The echo is then
52
- * reconstructed against the synced text, which is sound only for a
53
- * position-independent (full-text) change.
50
+ * `undefined` only when that buffer is unknown, because a rejection or
51
+ * {@link LanguageClientShadow.invalidateClientText} dropped what was
52
+ * tracked. The echo is then reconstructed against the synced text, which
53
+ * is sound only for a position-independent (full-text) change.
54
54
  */
55
55
  readonly before: string | undefined;
56
56
  /** {@link textHash} of the text this push moves the client to. */
@@ -97,7 +97,7 @@ export type LanguageClientChangeVerdict =
97
97
 
98
98
  /**
99
99
  * The store's model of what the LSP language client holds for each URI it
100
- * opened or the store pushed to: the text, the version it declared and the version a push moved it
100
+ * opened: the text, the version it declared and the version a push moved it
101
101
  * to, and the pushes it has not echoed yet. The store speaks the protocol;
102
102
  * this answers what an incoming change is and what an outgoing push sends.
103
103
  *
@@ -112,10 +112,10 @@ export type LanguageClientChangeVerdict =
112
112
  export interface LanguageClientShadow {
113
113
  /**
114
114
  * The client opened `key` under `clientUri`. `firstOpen`, the store's first
115
- * open of `key`: its text becomes the diff baseline, unless a push already
116
- * waits for this URI; otherwise an equality-only baseline, since a buffer
117
- * opened from disk can lag the synced text. A no-op when this URI is
118
- * already open.
115
+ * open of `key`: its text becomes the diff baseline; otherwise an
116
+ * equality-only baseline, since a buffer opened from disk can lag the
117
+ * synced text. Either replaces what was tracked for `clientUri` before the
118
+ * open. A no-op when this URI is already open.
119
119
  */
120
120
  addOpen(key: CanonicalUri, clientUri: LanguageClientUri, version: number, text: string, firstOpen: boolean): void;
121
121
  /** The version the client last declared for `key` under `clientUri`; `undefined` when it never opened it there. */
@@ -140,12 +140,18 @@ export interface LanguageClientShadow {
140
140
  isOpen(key: CanonicalUri, clientUri?: LanguageClientUri): boolean;
141
141
  /** Forget every URI the client holds `key` under. */
142
142
  removeAllOpens(key: CanonicalUri): void;
143
- /** The URIs a push of `key` goes to: each open of it, else `fallback`. */
144
- pushTargets(key: CanonicalUri, fallback: LanguageClientUri): LanguageClientUri[];
143
+ /**
144
+ * The URIs a push of `key` goes to: each open of it, none when the client
145
+ * has not opened it. A client applies a push to a closed file by opening
146
+ * it, and an open racing its own push leaves no text to diff against.
147
+ */
148
+ pushTargets(key: CanonicalUri): LanguageClientUri[];
145
149
  /**
146
150
  * Diff `text` against what the client holds under `clientUri` and queue the
147
151
  * push for echo correlation, which has to precede the RPC because an echo
148
- * can arrive before its response. `undefined` when there is nothing to send.
152
+ * can arrive before its response. `undefined` when there is nothing to send,
153
+ * which includes a `clientUri` the client no longer has open: a close can
154
+ * land while the push to another of its URIs is in flight.
149
155
  */
150
156
  preparePush(key: CanonicalUri, clientUri: LanguageClientUri, text: string): PreparedLanguageClientPush | undefined;
151
157
  /** Forget what the client holds under `clientUri`; its next push is a full replace. */
@@ -209,12 +215,13 @@ export class DefaultLanguageClientShadow<T extends TextDocument = TextDocument>
209
215
  this.opens.set(key, uris);
210
216
  }
211
217
  uris.set(clientUri, { declaredVersion: version });
212
- if (!firstOpen) {
213
- this.openedTexts.set(clientUri, text);
214
- } else if (!this.baselines.has(clientUri)) {
215
- // A push to a closed file has the client open it from disk and apply
216
- // afterwards, so a tracked text already names what it is about to hold.
218
+ // Dropped: a seed set before the open names no buffer, and a line diff
219
+ // against it splices text the client does not hold.
220
+ this.invalidateClientText(clientUri);
221
+ if (firstOpen) {
217
222
  this.baselines.set(clientUri, text);
223
+ } else {
224
+ this.openedTexts.set(clientUri, text);
218
225
  }
219
226
  }
220
227
 
@@ -312,12 +319,16 @@ export class DefaultLanguageClientShadow<T extends TextDocument = TextDocument>
312
319
  this.opens.delete(key);
313
320
  }
314
321
 
315
- pushTargets(key: CanonicalUri, fallback: LanguageClientUri): LanguageClientUri[] {
316
- const uris = this.opens.get(key);
317
- return uris && uris.size > 0 ? [...uris.keys()] : [fallback];
322
+ pushTargets(key: CanonicalUri): LanguageClientUri[] {
323
+ return [...(this.opens.get(key)?.keys() ?? [])];
318
324
  }
319
325
 
320
326
  preparePush(key: CanonicalUri, clientUri: LanguageClientUri, text: string): PreparedLanguageClientPush | undefined {
327
+ // Captured now: a reopen while the push is in flight replaces the state.
328
+ const state = this.opens.get(key)?.get(clientUri);
329
+ if (state === undefined) {
330
+ return undefined;
331
+ }
321
332
  // Read before computing the edits, which moves the baseline: this is the
322
333
  // only text the push's echo can be reconstructed against.
323
334
  const before = this.clientText(clientUri);
@@ -326,12 +337,9 @@ export class DefaultLanguageClientShadow<T extends TextDocument = TextDocument>
326
337
  return undefined;
327
338
  }
328
339
  this.recordPendingPush(clientUri, before, text);
329
- // Captured now: a reopen while the push is in flight replaces the state.
330
- const state = this.opens.get(key)?.get(clientUri);
331
340
  // Gating a full replace turns a stale-by-one version into a refused
332
341
  // update for no safety gain, and refuses the retry after a rejection.
333
- const version =
334
- isFullReplace(edits) || state === undefined ? null : Math.max(state.declaredVersion, state.pushedVersion ?? state.declaredVersion);
342
+ const version = isFullReplace(edits) ? null : Math.max(state.declaredVersion, state.pushedVersion ?? state.declaredVersion);
335
343
  let settled = false;
336
344
  return {
337
345
  clientUri,
@@ -344,7 +352,7 @@ export class DefaultLanguageClientShadow<T extends TextDocument = TextDocument>
344
352
  this.tracer
345
353
  .with(key)
346
354
  .warn(
347
- `Language client refused applyEdit addressed at version ${version} (it last declared version ${state?.declaredVersion})`
355
+ `Language client refused applyEdit addressed at version ${version} (it last declared version ${state.declaredVersion})`
348
356
  );
349
357
  }
350
358
  this.settle(clientUri, version, state, outcome);
@@ -59,12 +59,12 @@ export namespace IntegrityPhase {
59
59
  * correction. Otherwise — the holder has unsaved edits, disk changed behind
60
60
  * the server, or disk cannot be read — the correction waits in the text
61
61
  * store for the next save.
62
- * - `editor`: open the file dirty via `applyEdit`. If the user closes without
63
- * saving, the correction is suppressed until the file changes or the
64
- * workspace reloads. A file held only through the data or GLSP head gets
65
- * neither: the correction stays in the text store, and since those heads
66
- * mark only their own edits unsaved, nothing shows that store and disk
67
- * differ until a save or the last close.
62
+ * - `editor`: leave disk alone and stage the correction; the next open of the
63
+ * file takes it over the disk text, and an editor shows it as an unsaved
64
+ * change. Until then no editor sees it. A file held only through the data
65
+ * or GLSP head is not staged: the correction stays in the text store, and
66
+ * since those heads mark only their own edits unsaved, nothing shows that
67
+ * store and disk differ until a save or the last close.
68
68
  */
69
69
  export type IntegritySyncMode = 'silent' | 'editor';
70
70
 
@@ -469,7 +469,8 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
469
469
  * otherwise it waits for the next save. Editor mode always waits, and the
470
470
  * repair leaves the document dirty until a save.
471
471
  * - **Closed, silent mode** → write to disk directly (disk is authoritative).
472
- * - **Closed, editor mode** → stage so the next open picks it up via applyEdit.
472
+ * - **Closed, editor mode** → stage, so the next open takes the correction
473
+ * over the disk text.
473
474
  *
474
475
  * A held URI is not closed. Writing it as one persists the holder's unsaved
475
476
  * edits with the repair, and staging it leaves text no open reads, because
@@ -520,7 +521,7 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
520
521
  return;
521
522
  }
522
523
 
523
- // Closed files, editor mode: stage content so the next open picks it up via applyEdit.
524
+ // Closed files, editor mode: stage content for the next open.
524
525
  this.textDocuments.stagePendingContent(document.uri, document.getText());
525
526
  }
526
527
 
@@ -262,6 +262,11 @@ export interface ModelService<
262
262
  validated(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
263
263
 
264
264
  isOpen(uri: string): boolean;
265
+ /**
266
+ * The version the document at `uri` was opened at, which a client's write
267
+ * or an integrity repair steps past; `undefined` while it is not open.
268
+ */
269
+ openedVersion(uri: string): TextVersion | undefined;
265
270
  snapshot(uri: string): AstDocument<TAst, TDiagnostic> | undefined;
266
271
  /**
267
272
  * The text a session's write of `model` to `uri` applies: a textual model
@@ -839,6 +844,10 @@ export class DefaultModelService<
839
844
  return this.services.workspace.AstDocumentManager.isOpen(uri);
840
845
  }
841
846
 
847
+ openedVersion(uri: string): TextVersion | undefined {
848
+ return this.services.workspace.TextDocuments.openedVersion(uri);
849
+ }
850
+
842
851
  /**
843
852
  * Snapshot of `uri` as it stands RIGHT NOW — the synchronous sibling of the
844
853
  * phase reads, which all wait. `undefined` when no document is registered,
@@ -89,6 +89,7 @@ export interface StubHydraniumTextDocuments extends Pick<
89
89
  | 'onDidReleaseDocument'
90
90
  | 'isDirty'
91
91
  | 'textState'
92
+ | 'openedVersion'
92
93
  | 'onDidChangeDirty'
93
94
  | 'setDiskBaseline'
94
95
  > {
@@ -135,15 +136,20 @@ export interface StubHydraniumTextDocuments extends Pick<
135
136
  /**
136
137
  * Decide what {@link applyEditToLanguageClient} answers, so a suite can drive
137
138
  * the rejection path (`{ applied: false }`) the real client takes when its
138
- * buffer has outrun the version the push was addressed at. The call is still
139
- * recorded in {@link appliedEdits} either way. Defaults to accepting.
139
+ * buffer has outrun the version the push was addressed at. The push is still
140
+ * recorded in {@link appliedEdits} either way. Defaults to accepting; not
141
+ * called for a URI not open in the language client.
140
142
  *
141
143
  * The handler runs INSIDE the call, which is also the only place a suite can
142
144
  * simulate something happening while the push is in flight — a newer settle,
143
145
  * a concurrent client edit.
144
146
  */
145
147
  setApplyEditHandler(handler: (uri: string, text: string) => ApplyWorkspaceEditResult | undefined): void;
146
- /** Replay of every {@link applyEditToLanguageClient} call (the text channel to Monaco). */
148
+ /**
149
+ * Replay of every push {@link applyEditToLanguageClient} sent (the text
150
+ * channel to Monaco): none for a URI not seeded open in the language client,
151
+ * as the real store sends none.
152
+ */
147
153
  readonly appliedEdits: readonly { uri: string; text: string; label?: string }[];
148
154
  /** Replay of every {@link stagePendingContent} call (the not-yet-open staging path). */
149
155
  readonly staged: readonly { uri: string; text: string }[];
@@ -167,6 +173,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
167
173
  const key = (uri: string): CanonicalUri => uri as CanonicalUri;
168
174
  const documentReleased = new Emitter<DocumentReleasedEvent>();
169
175
  const baselines = new Map<string, string | undefined>();
176
+ const openedVersions = new Map<string, number>();
170
177
  const dirty = new Set<string>();
171
178
  const dirtyChanged = new Emitter<DocumentDirtyChangedEvent>();
172
179
  // As the real store: dirty while held with text other than the baseline,
@@ -209,6 +216,9 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
209
216
  version(uri) {
210
217
  return docs.get(uri)?.version ?? 0;
211
218
  },
219
+ openedVersion(uri) {
220
+ return docs.has(uri) ? openedVersions.get(uri) : undefined;
221
+ },
212
222
  textState(uri) {
213
223
  const held = docs.get(uri);
214
224
  return held && { version: held.version, hash: textHash(held.text), dirty: dirty.has(uri) };
@@ -331,6 +341,9 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
331
341
  applyEditHandler = handler;
332
342
  },
333
343
  async applyEditToLanguageClient(uri, newText, options): Promise<ApplyWorkspaceEditResult | undefined> {
344
+ if (!languageClientOpen.has(uri)) {
345
+ return undefined;
346
+ }
334
347
  appliedEdits.push({ uri, text: newText, label: options?.label });
335
348
  return applyEditHandler(uri, newText);
336
349
  },
@@ -340,6 +353,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
340
353
  seedOpen(uri, text, clientId) {
341
354
  sessions.addOpen(key(uri), clientId);
342
355
  docs.set(uri, { uri, version: 1, text, clientId });
356
+ openedVersions.set(uri, 1);
343
357
  baselines.set(uri, text);
344
358
  },
345
359
  seedOpenInLanguageClient(uri) {
@@ -379,6 +393,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
379
393
  appliedEdits.length = 0;
380
394
  staged.length = 0;
381
395
  baselines.clear();
396
+ openedVersions.clear();
382
397
  dirty.clear();
383
398
  saveListeners.length = 0;
384
399
  closeListeners.length = 0;