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

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 (99) hide show
  1. package/lib/documents/ast-document-manager.js +4 -4
  2. package/lib/documents/ast-document-manager.js.map +1 -1
  3. package/lib/documents/client-ids.d.ts +1 -1
  4. package/lib/documents/client-ids.d.ts.map +1 -1
  5. package/lib/documents/client-ids.js +1 -1
  6. package/lib/documents/client-ids.js.map +1 -1
  7. package/lib/documents/client-session-registry.d.ts +24 -17
  8. package/lib/documents/client-session-registry.d.ts.map +1 -1
  9. package/lib/documents/client-session-registry.js +49 -29
  10. package/lib/documents/client-session-registry.js.map +1 -1
  11. package/lib/documents/dirty-state-tracker.d.ts +79 -0
  12. package/lib/documents/dirty-state-tracker.d.ts.map +1 -0
  13. package/lib/documents/dirty-state-tracker.js +69 -0
  14. package/lib/documents/dirty-state-tracker.js.map +1 -0
  15. package/lib/documents/document-release-handler.d.ts +174 -0
  16. package/lib/documents/document-release-handler.d.ts.map +1 -0
  17. package/lib/documents/document-release-handler.js +270 -0
  18. package/lib/documents/document-release-handler.js.map +1 -0
  19. package/lib/documents/document-release-scheduler.d.ts +67 -0
  20. package/lib/documents/document-release-scheduler.d.ts.map +1 -0
  21. package/lib/documents/document-release-scheduler.js +80 -0
  22. package/lib/documents/document-release-scheduler.js.map +1 -0
  23. package/lib/documents/hydranium-text-documents.d.ts +130 -455
  24. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  25. package/lib/documents/hydranium-text-documents.js +362 -835
  26. package/lib/documents/hydranium-text-documents.js.map +1 -1
  27. package/lib/documents/index.d.ts +5 -1
  28. package/lib/documents/index.d.ts.map +1 -1
  29. package/lib/documents/index.js +5 -1
  30. package/lib/documents/index.js.map +1 -1
  31. package/lib/documents/language-client-shadow.d.ts +221 -0
  32. package/lib/documents/language-client-shadow.d.ts.map +1 -0
  33. package/lib/documents/language-client-shadow.js +288 -0
  34. package/lib/documents/language-client-shadow.js.map +1 -0
  35. package/lib/documents/text-ledger.d.ts +64 -0
  36. package/lib/documents/text-ledger.d.ts.map +1 -0
  37. package/lib/documents/text-ledger.js +63 -0
  38. package/lib/documents/text-ledger.js.map +1 -0
  39. package/lib/langium/document-builder/document-builder.d.ts +8 -7
  40. package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
  41. package/lib/langium/document-builder/document-builder.js +8 -7
  42. package/lib/langium/document-builder/document-builder.js.map +1 -1
  43. package/lib/langium/integrity/integrity-service.d.ts +1 -1
  44. package/lib/langium/integrity/integrity-service.js +4 -4
  45. package/lib/langium/integrity/integrity-service.js.map +1 -1
  46. package/lib/langium/model-service/client-session.d.ts +4 -4
  47. package/lib/langium/model-service/client-session.js +2 -2
  48. package/lib/langium/model-service/client-session.js.map +1 -1
  49. package/lib/langium/model-service/model-events.d.ts +8 -11
  50. package/lib/langium/model-service/model-events.d.ts.map +1 -1
  51. package/lib/langium/model-service/model-service.d.ts +12 -4
  52. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  53. package/lib/langium/model-service/model-service.js +1 -1
  54. package/lib/langium/model-service/model-service.js.map +1 -1
  55. package/lib/langium/module.d.ts +7 -0
  56. package/lib/langium/module.d.ts.map +1 -1
  57. package/lib/langium/module.js +3 -1
  58. package/lib/langium/module.js.map +1 -1
  59. package/lib/langium/workspace/file-not-found.d.ts +7 -1
  60. package/lib/langium/workspace/file-not-found.d.ts.map +1 -1
  61. package/lib/langium/workspace/file-not-found.js +14 -3
  62. package/lib/langium/workspace/file-not-found.js.map +1 -1
  63. package/lib/lsp/hydranium-document-update-handler.d.ts +6 -6
  64. package/lib/lsp/hydranium-document-update-handler.js +10 -10
  65. package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
  66. package/lib/testing/make-test-services.d.ts +3 -0
  67. package/lib/testing/make-test-services.d.ts.map +1 -1
  68. package/lib/testing/make-test-services.js +5 -2
  69. package/lib/testing/make-test-services.js.map +1 -1
  70. package/lib/testing/stub-hydranium-text-documents.d.ts +9 -6
  71. package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
  72. package/lib/testing/stub-hydranium-text-documents.js +19 -11
  73. package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
  74. package/package.json +5 -5
  75. package/src/documents/ast-document-manager.ts +4 -4
  76. package/src/documents/client-ids.ts +1 -1
  77. package/src/documents/client-session-registry.ts +55 -34
  78. package/src/documents/dirty-state-tracker.ts +130 -0
  79. package/src/documents/document-release-handler.ts +351 -0
  80. package/src/documents/document-release-scheduler.ts +121 -0
  81. package/src/documents/hydranium-text-documents.ts +395 -1034
  82. package/src/documents/index.ts +5 -1
  83. package/src/documents/language-client-shadow.ts +463 -0
  84. package/src/documents/text-ledger.ts +112 -0
  85. package/src/langium/document-builder/document-builder.ts +8 -7
  86. package/src/langium/integrity/integrity-service.ts +4 -4
  87. package/src/langium/model-service/client-session.ts +6 -6
  88. package/src/langium/model-service/model-events.ts +12 -11
  89. package/src/langium/model-service/model-service.ts +13 -4
  90. package/src/langium/module.ts +9 -1
  91. package/src/langium/workspace/file-not-found.ts +15 -3
  92. package/src/lsp/hydranium-document-update-handler.ts +10 -10
  93. package/src/testing/make-test-services.ts +7 -2
  94. package/src/testing/stub-hydranium-text-documents.ts +34 -21
  95. package/lib/documents/language-client-text-shadow.d.ts +0 -133
  96. package/lib/documents/language-client-text-shadow.d.ts.map +0 -1
  97. package/lib/documents/language-client-text-shadow.js +0 -201
  98. package/lib/documents/language-client-text-shadow.js.map +0 -1
  99. package/src/documents/language-client-text-shadow.ts +0 -223
@@ -80,7 +80,7 @@ export interface ClientSession<
80
80
 
81
81
  /**
82
82
  * Open `uri` for this session, reading it from disk unless some client has
83
- * it open already or it holds this client's unsaved text within its revert
83
+ * it open already or it holds this client's unsaved text within its release
84
84
  * grace. `options` are kept for this open until it closes; a repeat open,
85
85
  * concurrent or not, keeps the options of the first.
86
86
  */
@@ -90,7 +90,7 @@ export interface ClientSession<
90
90
  /**
91
91
  * Create a document with `text` and open it for this session. It reaches
92
92
  * disk on the first `save`. Fails when the file exists, any client has the
93
- * URI open, or the URI waits out the revert grace, and of two creates of one
93
+ * URI open, or the URI waits out the release grace, and of two creates of one
94
94
  * URI at most one succeeds.
95
95
  *
96
96
  * Resolves with the version the created document took, which a write based
@@ -137,8 +137,8 @@ export interface ClientSession<
137
137
  * End the session: close everything it has open and free its id.
138
138
  * Idempotent. `cause` is `'lost'` when the session ends because its
139
139
  * client's connection went away, which lets each document it was the last
140
- * to have open wait out the store's revert grace; ending it for any other
141
- * reason reverts such a document at once.
140
+ * to have open wait out the store's release grace; ending it for any other
141
+ * reason releases such a document at once.
142
142
  */
143
143
  dispose(cause?: SessionEndCause): void;
144
144
  /**
@@ -497,8 +497,8 @@ export class DefaultClientSession<
497
497
  throw new Error(`Cannot create ${uri}: the file exists`);
498
498
  }
499
499
  this.assertLive();
500
- if (this.services.workspace.TextDocuments.isRevertPending(uri)) {
501
- throw new Error(`Cannot create ${uri}: it holds the unsaved text of a lost client, waiting out the revert grace`);
500
+ if (this.services.workspace.TextDocuments.isReleaseDeferred(uri)) {
501
+ throw new Error(`Cannot create ${uri}: it holds the unsaved text of a lost client, waiting out the release grace`);
502
502
  }
503
503
  if (this.modelService.isOpen(uri)) {
504
504
  throw new Error(`Cannot create ${uri}: it is open in a client`);
@@ -9,8 +9,9 @@
9
9
 
10
10
  import { type CanonicalUri } from '@hydranium/protocol';
11
11
  import { type AstNode, type DocumentState } from '@hydranium/langium';
12
- import { type AstDocumentUpdatedEvent } from '../../documents/ast-document-manager.js';
13
- import { type DocumentDirtyChangedEvent } from '../../documents/hydranium-text-documents.js';
12
+ import { type AstDocumentSavedEvent, type AstDocumentUpdatedEvent } from '../../documents/ast-document-manager.js';
13
+ import { type DocumentDirtyChangedEvent } from '../../documents/dirty-state-tracker.js';
14
+ import { type DocumentReleasedEvent } from '../../documents/hydranium-text-documents.js';
14
15
  import { type AstDiagnostic } from '../validation/document-validator.js';
15
16
 
16
17
  /** Which documents a `ModelService` subscription hears about. */
@@ -61,14 +62,14 @@ export interface ModelDeletedEvent {
61
62
  readonly uri: CanonicalUri;
62
63
  }
63
64
 
64
- /** A document's text started or stopped differing from its file. */
65
+ /** A document was saved: the AST manager's event, as `ModelService` hands it out. */
66
+ export type ModelSavedEvent<TAst extends AstNode, TDiagnostic extends AstDiagnostic = AstDiagnostic> = AstDocumentSavedEvent<
67
+ TAst,
68
+ TDiagnostic
69
+ >;
70
+
71
+ /** A document's text started or stopped differing from its file: the store's event, as `ModelService` hands it out. */
65
72
  export type ModelDirtyChangedEvent = DocumentDirtyChangedEvent;
66
73
 
67
- /**
68
- * A document no client has open any more was released and reverts to disk.
69
- * The rebuild that reverts it follows as a {@link ModelUpdatedEvent}, unless a
70
- * client opens the document first or its file is deleted.
71
- */
72
- export interface ModelReleasedEvent {
73
- readonly uri: CanonicalUri;
74
- }
74
+ /** A document no client has open any more was released: the store's event, as `ModelService` hands it out. */
75
+ export type ModelReleasedEvent = DocumentReleasedEvent;
@@ -25,7 +25,7 @@ import { type AstDiagnostic } from '../validation/document-validator.js';
25
25
  import { type DocumentUriPolicy } from '../workspace/document-uri-policy.js';
26
26
  import { ReentrantWriteLockError, isInsideWriteLock, isWriteLockScopeInstalled } from '../workspace/write-lock-scope.js';
27
27
  import { CancellationToken, Disposable } from 'vscode-languageserver';
28
- import { AstDocument, type AstDocumentSavedEvent } from '../../documents/ast-document-manager.js';
28
+ import { AstDocument } from '../../documents/ast-document-manager.js';
29
29
  import { isConnectionGoneError } from '../../util/connection-liveness.js';
30
30
  import { type LogNameOptions } from '../diagnostics/logger.js';
31
31
  import { IntegrityService } from '../integrity/integrity-service.js';
@@ -40,6 +40,7 @@ import {
40
40
  type ModelEventFilter,
41
41
  type ModelPhaseFilter,
42
42
  type ModelReleasedEvent,
43
+ type ModelSavedEvent,
43
44
  type ModelsBuiltEvent,
44
45
  type ModelUpdatedEvent
45
46
  } from './model-events.js';
@@ -285,9 +286,17 @@ export interface ModelService<
285
286
  * it. A document saved before its first build arrives as an empty envelope
286
287
  * at `UNRECORDED_VERSION`, whose `root` is `undefined` despite its type.
287
288
  */
288
- onModelSaved(listener: (event: AstDocumentSavedEvent<TAst, TDiagnostic>) => void, filter?: ModelEventFilter): Disposable;
289
+ onModelSaved(listener: (event: ModelSavedEvent<TAst, TDiagnostic>) => void, filter?: ModelEventFilter): Disposable;
289
290
  onModelDeleted(listener: (event: ModelDeletedEvent) => void, filter?: ModelEventFilter): Disposable;
291
+ /** Fires when a document's text starts or stops differing from its file. */
290
292
  onDirtyChanged(listener: (event: ModelDirtyChangedEvent) => void, filter?: ModelEventFilter): Disposable;
293
+ /**
294
+ * Fires when the store releases a document no client has open any more.
295
+ * What the build keeps for it is up to the
296
+ * `DocumentReleaseHandler` slot; the default reverts it to disk, and that
297
+ * rebuild follows as an {@link onModelUpdated} event, unless a client opens
298
+ * the document first or its file is deleted.
299
+ */
291
300
  onModelReleased(listener: (event: ModelReleasedEvent) => void, filter?: ModelEventFilter): Disposable;
292
301
  /** Fires when `filter.clientId` closes `filter.uri`, its session ending included. */
293
302
  onClientClosed(listener: () => void, filter: { readonly uri: string; readonly clientId: string }): Disposable;
@@ -1145,7 +1154,7 @@ export class DefaultModelService<
1145
1154
  });
1146
1155
  }
1147
1156
 
1148
- onModelSaved(listener: (event: AstDocumentSavedEvent<TAst, TDiagnostic>) => void, filter: ModelEventFilter = {}): Disposable {
1157
+ onModelSaved(listener: (event: ModelSavedEvent<TAst, TDiagnostic>) => void, filter: ModelEventFilter = {}): Disposable {
1149
1158
  const target = this.filterUri(filter);
1150
1159
  return this.services.workspace.TextDocuments.onDidSave(event => {
1151
1160
  // LangiumDocuments keys by the canonical form, the saved event by the
@@ -1180,7 +1189,7 @@ export class DefaultModelService<
1180
1189
 
1181
1190
  onModelReleased(listener: (event: ModelReleasedEvent) => void, filter: ModelEventFilter = {}): Disposable {
1182
1191
  const target = this.filterUri(filter);
1183
- return this.services.workspace.TextDocuments.onDidCloseLastOpen(event => {
1192
+ return this.services.workspace.TextDocuments.onDidReleaseDocument(event => {
1184
1193
  if (this.matches(target, event.uri)) {
1185
1194
  listener(event);
1186
1195
  }
@@ -38,6 +38,7 @@ import { DefaultAstDocumentManager, type AstDocumentManager } from '../documents
38
38
  import { DefaultSelfSaveRegistry, type SelfSaveRegistry } from './workspace/self-save-registry.js';
39
39
  import { DefaultFileSystemTaskQueue, type FileSystemTaskQueue } from '../documents/file-system-task-queue.js';
40
40
  import { DefaultModelLedger, type ModelLedger } from '../documents/model-ledger.js';
41
+ import { DefaultDocumentReleaseHandler, type DocumentReleaseHandler } from '../documents/document-release-handler.js';
41
42
  import { DefaultVersionSyncService, type VersionSyncService } from '../documents/version-sync-service.js';
42
43
  import {
43
44
  DefaultEmptyFileSystemProvider,
@@ -286,6 +287,12 @@ export interface ServerAddedSharedServices<
286
287
  ModelLedger: ModelLedger;
287
288
  /** Reconciles each produced root with the store, and owns every build that syncs a root to its text or recovers one. */
288
289
  VersionSyncService: VersionSyncService;
290
+ /**
291
+ * What the build keeps for a document {@link HydraniumTextDocuments} has
292
+ * released, for every head. Bound to {@link DefaultDocumentReleaseHandler},
293
+ * which reverts it to disk.
294
+ */
295
+ DocumentReleaseHandler: DocumentReleaseHandler;
289
296
  };
290
297
  /**
291
298
  * In-process workspace facade ({@link ModelService}) + AST→transfer
@@ -562,7 +569,8 @@ export function createServerSharedModule(
562
569
  // first build.
563
570
  CstResidencyService: services => new DefaultCstResidencyService(services),
564
571
  ModelLedger: () => new DefaultModelLedger(),
565
- VersionSyncService: services => new DefaultVersionSyncService(services)
572
+ VersionSyncService: services => new DefaultVersionSyncService(services),
573
+ DocumentReleaseHandler: services => new DefaultDocumentReleaseHandler(services)
566
574
  },
567
575
  model: {
568
576
  // Generic walker — adopters with a typed `$type → wire shape` overlay
@@ -9,15 +9,27 @@
9
9
 
10
10
  import { type URI } from '@hydranium/langium';
11
11
 
12
- // Not re-exported from the workspace barrel: the framework's providers share
13
- // it, and it is no API of theirs.
12
+ // Not re-exported from the workspace barrel: the framework's providers and
13
+ // the release handler share it, and it is no API of theirs.
14
14
 
15
15
  /**
16
16
  * A missing-file error shaped as a Node file system raises it, with code
17
17
  * `ENOENT` and the path: callers tell a missing file from any other failure by
18
- * those two fields, whichever provider is bound. The last close relies on it to
18
+ * those two fields, whichever provider is bound. The release relies on it to
19
19
  * tell a document gone meanwhile from any other failed rebuild.
20
20
  */
21
21
  export function notFound(message: string, uri: URI): Error {
22
22
  return Object.assign(new Error(message), { code: 'ENOENT', path: uri.fsPath });
23
23
  }
24
+
25
+ /**
26
+ * Whether `err` reports that the file of `target` does not exist, as a Node
27
+ * file system and {@link notFound} do: code `ENOENT`, with `target`'s path. A
28
+ * missing file of another document is a different failure.
29
+ */
30
+ export function isFileNotFound(err: unknown, target: URI): boolean {
31
+ if (typeof err !== 'object' || err === null || !('code' in err) || err.code !== 'ENOENT') {
32
+ return false;
33
+ }
34
+ return !('path' in err) || err.path === target.fsPath;
35
+ }
@@ -93,12 +93,12 @@ export interface HydraniumDocumentUpdateHandlerOptions {
93
93
  * Both waits are capped by
94
94
  * {@link HydraniumDocumentUpdateHandlerOptions.willSaveGateMs}.
95
95
  *
96
- * The revert to disk after a document's last close is not dispatched here:
97
- * the text store runs it for every head, the LSP head included, so a
98
- * `didCloseDocument` added in a subclass would build the document twice. A
99
- * change still debounced for the document is dropped when the store releases
100
- * it: the store's revert rebuilds the document from the file system provider
101
- * or removes it.
96
+ * What the build keeps after a document's last close is not dispatched here:
97
+ * the store hands the released document to the `DocumentReleaseHandler` for
98
+ * every head, the LSP head included, so a `didCloseDocument` added in a
99
+ * subclass would build the document twice. A change still debounced for the
100
+ * document is dropped when the store releases it: the release handler rebuilds
101
+ * the document from the file system provider or removes it.
102
102
  *
103
103
  * Adopters with their own handler subclass should extend this class
104
104
  * (not Langium's `DefaultDocumentUpdateHandler`) so all of them are
@@ -175,10 +175,10 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
175
175
  uri => this.pendingChanged.has(uri.toString()) || this.pendingDeleted.has(uri.toString())
176
176
  );
177
177
  // A change still debounced for a document the store has released would
178
- // build it once more after the store's own revert: from the provider
179
- // again, or, for a document the revert removed, by reading a file that
180
- // is not there.
181
- this.textDocuments.onDidCloseLastOpen(event => {
178
+ // build it once more after the release handler's build: from the
179
+ // provider again, or, for a document the release removed, by reading a
180
+ // file that is not there.
181
+ this.textDocuments.onDidReleaseDocument(event => {
182
182
  this.pendingChanged.delete(URI.parse(event.uri).toString());
183
183
  if (this.pendingChanged.size === 0 && this.pendingDeleted.size === 0) {
184
184
  this.pendingReason = undefined;
@@ -36,6 +36,7 @@ import { DefaultDocumentUriPolicy, type DocumentUriPolicy } from '../langium/wor
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
38
  import { DefaultModelLedger, type ModelLedger } from '../documents/model-ledger.js';
39
+ import { DefaultDocumentReleaseHandler, type DocumentReleaseHandler } from '../documents/document-release-handler.js';
39
40
  import { DefaultVersionSyncService, type VersionSyncService } from '../documents/version-sync-service.js';
40
41
  import type { FakeDocumentOptions } from './fake-document.js';
41
42
  import { makeStubDocumentBuilder, type StubDocumentBuilder } from './stub-document-builder.js';
@@ -115,6 +116,8 @@ export interface TestSharedServices<
115
116
  ModelLedger: ModelLedger;
116
117
  /** The REAL service, over this tree's stubs; it reconciles nothing against the stub store. */
117
118
  VersionSyncService: VersionSyncService;
119
+ /** The REAL handler, over this tree's stubs; the stub store releases nothing to it. */
120
+ DocumentReleaseHandler: DocumentReleaseHandler;
118
121
  };
119
122
  readonly model: {
120
123
  TransferEncoder: TransferEncoder<TTransferDiagnostic>;
@@ -378,8 +381,9 @@ export function makeTestServices<
378
381
  WorkspaceLock: workspaceLock,
379
382
  FileSystemTaskQueue: new DefaultFileSystemTaskQueue({ workspace: { DocumentUriPolicy: documentUriPolicy } }),
380
383
  ModelLedger: modelLedger,
381
- // Patched in below: it reads the tree it belongs to.
382
- VersionSyncService: {} as VersionSyncService
384
+ // Patched in below, as both read the tree they belong to.
385
+ VersionSyncService: {} as VersionSyncService,
386
+ DocumentReleaseHandler: {} as DocumentReleaseHandler
383
387
  },
384
388
  model: {} as TestSharedServices<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic>['model'],
385
389
  ServerLocale: {} as ServerLocale,
@@ -390,6 +394,7 @@ export function makeTestServices<
390
394
  // Patched in after the literal, like `model` below: both read the tree they
391
395
  // belong to, and the renderer reads the locale service.
392
396
  services.workspace.VersionSyncService = new DefaultVersionSyncService(sharedServices);
397
+ services.workspace.DocumentReleaseHandler = new DefaultDocumentReleaseHandler(sharedServices);
393
398
  const mutableMessages = services as { ServerLocale: ServerLocale; MessageRenderer: MessageRenderer };
394
399
  const serverLocale = new DefaultServerLocale(sharedServices);
395
400
  if (options.locale) {
@@ -13,11 +13,11 @@ import { Disposable, Emitter } from 'vscode-languageserver';
13
13
  import type { TextDocument } from 'vscode-languageserver-textdocument';
14
14
  import { LANGUAGE_CLIENT_ID } from '../documents/client-ids.js';
15
15
  import { ClientSessionRegistry } from '../documents/client-session-registry.js';
16
+ import type { DocumentDirtyChangedEvent } from '../documents/dirty-state-tracker.js';
16
17
  import type {
17
18
  ClientTextDocumentChangeEvent,
18
- DocumentDirtyChangedEvent,
19
19
  HydraniumTextDocuments,
20
- LastOpenClosedEvent
20
+ DocumentReleasedEvent
21
21
  } from '../documents/hydranium-text-documents.js';
22
22
 
23
23
  /** Snapshot of an open document tracked by the stub. */
@@ -33,11 +33,11 @@ export interface StubTextDocumentEntry {
33
33
  * reads from on test paths — the content channel
34
34
  * (`notifyDidChangeTextDocument` / `applyContentChange` / `version` /
35
35
  * `getAuthor`), the open-state probes (`isOpenInLanguageClient` /
36
- * `isOpenInAnyClient` / `isOpenInClient` / `isRevertPending` /
36
+ * `isOpenInAnyClient` / `isOpenInClient` / `isReleaseDeferred` /
37
37
  * `openDocuments`), the push channel
38
38
  * to the language client (`applyEditToLanguageClient` / `stagePendingContent`),
39
39
  * the save / close notifications, the dirty state (`isDirty` / `textState` /
40
- * `onDidChangeDirty` / `updateDiskBaseline`, against a baseline {@link seedOpen}
40
+ * `onDidChangeDirty` / `setDiskBaseline`, 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
43
43
  * `ClientSessionRegistry` — plus test-only helpers:
@@ -77,7 +77,7 @@ export interface StubHydraniumTextDocuments extends Pick<
77
77
  | 'getAuthor'
78
78
  | 'isOpenInLanguageClient'
79
79
  | 'isOpenInAnyClient'
80
- | 'isRevertPending'
80
+ | 'isReleaseDeferred'
81
81
  | 'applyEditToLanguageClient'
82
82
  | 'stagePendingContent'
83
83
  | 'openDocuments'
@@ -86,11 +86,11 @@ export interface StubHydraniumTextDocuments extends Pick<
86
86
  | 'registerSession'
87
87
  | 'closeSession'
88
88
  | 'onDidCloseSession'
89
- | 'onDidCloseLastOpen'
89
+ | 'onDidReleaseDocument'
90
90
  | 'isDirty'
91
91
  | 'textState'
92
92
  | 'onDidChangeDirty'
93
- | 'updateDiskBaseline'
93
+ | 'setDiskBaseline'
94
94
  > {
95
95
  /**
96
96
  * Stub-tailored read accessor. Returns just the surface the framework's
@@ -114,9 +114,12 @@ export interface StubHydraniumTextDocuments extends Pick<
114
114
  /** Mark `uri` as open in the LSP textual language client (drives {@link isOpenInLanguageClient}). */
115
115
  seedOpenInLanguageClient(uri: string): void;
116
116
  /**
117
- * Synchronously deliver `onDidClose` to subscribers, then `onDidCloseLastOpen`
118
- * when no client has `uri` open any more. The stub keeps no revert grace, so
119
- * a last close is announced at once.
117
+ * Synchronously deliver `onDidClose` to subscribers, then `onDidReleaseDocument`
118
+ * when no client has `uri` open any more, then the dirty flip of a dropped
119
+ * holder: the real store's order. The stub keeps no release grace and runs
120
+ * no release handler, so the flip comes at once and without text, where the
121
+ * real store announces it once the handler settles, with text while the
122
+ * build still has the document.
120
123
  */
121
124
  fireClose(uri: string, clientId: string): void;
122
125
  /**
@@ -162,7 +165,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
162
165
  // stand in for canonical URIs (one stub-boundary cast, like `fireClose`).
163
166
  const sessions = new ClientSessionRegistry();
164
167
  const key = (uri: string): CanonicalUri => uri as CanonicalUri;
165
- const lastOpenClosed = new Emitter<LastOpenClosedEvent>();
168
+ const documentReleased = new Emitter<DocumentReleasedEvent>();
166
169
  const baselines = new Map<string, string | undefined>();
167
170
  const dirty = new Set<string>();
168
171
  const dirtyChanged = new Emitter<DocumentDirtyChangedEvent>();
@@ -177,7 +180,12 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
177
180
  } else {
178
181
  dirty.delete(uri);
179
182
  }
180
- dirtyChanged.fire({ uri: key(uri), text: { version: held?.version ?? 0, hash: textHash(held?.text ?? ''), dirty: now } });
183
+ // No build behind the stub, so a document no longer held is announced without text.
184
+ dirtyChanged.fire(
185
+ held === undefined
186
+ ? { uri: key(uri) }
187
+ : { uri: key(uri), text: { version: held.version, hash: textHash(held.text), dirty: now } }
188
+ );
181
189
  }
182
190
  };
183
191
 
@@ -237,7 +245,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
237
245
  },
238
246
  notifyDidSaveTextDocument(event, clientId) {
239
247
  if (event.text !== undefined) {
240
- stub.updateDiskBaseline(event.textDocument.uri, event.text);
248
+ stub.setDiskBaseline(event.textDocument.uri, event.text);
241
249
  }
242
250
  saves.push({ uri: event.textDocument.uri, clientId });
243
251
  for (const listener of saveListeners.slice()) {
@@ -274,8 +282,9 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
274
282
  isOpenInClient(uri, clientId) {
275
283
  return sessions.isOpenIn(key(uri), clientId);
276
284
  },
277
- isRevertPending(uri) {
278
- return sessions.isRevertPending(key(uri));
285
+ isReleaseDeferred() {
286
+ // The stub keeps no release grace.
287
+ return false;
279
288
  },
280
289
  attachClient(uri, clientId) {
281
290
  return docs.has(uri) && sessions.addOpen(key(uri), clientId);
@@ -298,8 +307,8 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
298
307
  get onDidCloseSession() {
299
308
  return sessions.onDidCloseSession;
300
309
  },
301
- get onDidCloseLastOpen() {
302
- return lastOpenClosed.event;
310
+ get onDidReleaseDocument() {
311
+ return documentReleased.event;
303
312
  },
304
313
  isDirty(uri) {
305
314
  return dirty.has(uri);
@@ -307,7 +316,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
307
316
  get onDidChangeDirty() {
308
317
  return dirtyChanged.event;
309
318
  },
310
- updateDiskBaseline(uri, text) {
319
+ setDiskBaseline(uri, text) {
311
320
  if (docs.has(uri)) {
312
321
  baselines.set(uri, text);
313
322
  refreshDirty(uri);
@@ -342,9 +351,9 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
342
351
  // class: listeners detect the last-close transition by consulting
343
352
  // `isOpenInAnyClient`, which reads the already-decremented state.
344
353
  const holder = docs.get(uri);
345
- if (holder && holder.clientId === clientId) {
354
+ const dropped = holder !== undefined && holder.clientId === clientId;
355
+ if (dropped) {
346
356
  docs.delete(uri);
347
- refreshDirty(uri);
348
357
  }
349
358
  const removed = sessions.removeOpen(key(uri), clientId);
350
359
  if (clientId === LANGUAGE_CLIENT_ID) {
@@ -355,7 +364,11 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
355
364
  listener(event);
356
365
  }
357
366
  if (removed && !sessions.isOpen(key(uri))) {
358
- lastOpenClosed.fire({ uri: key(uri) });
367
+ documentReleased.fire({ uri: key(uri) });
368
+ }
369
+ // After the release event, in the order the real store announces them.
370
+ if (dropped) {
371
+ refreshDirty(uri);
359
372
  }
360
373
  },
361
374
  reset() {
@@ -1,133 +0,0 @@
1
- /********************************************************************************
2
- * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
- *
4
- * This program and the accompanying materials are made available under the
5
- * terms of the MIT License which is available in the project root.
6
- *
7
- * SPDX-License-Identifier: MIT
8
- ********************************************************************************/
9
- import { type TextEdit } from 'vscode-languageserver';
10
- import { TextDocument } from 'vscode-languageserver-textdocument';
11
- /**
12
- * The minimum surface the shadow needs from a `HydraniumTextDocuments`:
13
- * a `create` method that materialises a fresh text document via the
14
- * configured `TextDocumentsConfiguration`. Declared as a narrow
15
- * structural interface so the manager satisfies it without an explicit
16
- * import (sidestepping the circular `HydraniumTextDocuments → shadow → manager`
17
- * dependency) and so tests can stub it with a minimal object literal.
18
- */
19
- export interface ShadowDocumentSource {
20
- create(uri: string, languageId: string, version: number, content: string): TextDocument;
21
- }
22
- /**
23
- * Whether `edits` is the single full-document replace {@link LanguageClientTextShadow.computeEdits}
24
- * emits when it has no usable baseline.
25
- *
26
- * The distinction matters at the `workspace/applyEdit` egress: a full replace is
27
- * position-INDEPENDENT and therefore lands correctly on any client buffer, while
28
- * a line-keyed diff only lands correctly on the exact text it was diffed
29
- * against. Only the latter needs a client-version gate.
30
- */
31
- export declare function isFullReplace(edits: readonly TextEdit[]): boolean;
32
- /**
33
- * Tracks the text content the language client (typically Monaco) is believed to
34
- * currently hold so outgoing `workspace/applyEdit` requests can ship minimal
35
- * diffs instead of full-document replaces.
36
- *
37
- * Why it matters: a full-range replace on a moderately-sized YAML or DSL source
38
- * forces Monaco to re-tokenise the entire document (observed multi-second hangs
39
- * on large diagrams), whereas line-level diffs usually reduce that to a handful
40
- * of small edits.
41
- *
42
- * Update points the caller is responsible for:
43
- * - {@link computeEdits} after each outgoing server→client sync (updates the shadow optimistically).
44
- * - {@link set} when the server receives a `didChange` from the language client (the client now
45
- * holds that text; we shouldn't resend it).
46
- * - {@link setOpenedText} when the client opens a document the server did not baseline for it —
47
- * an equality-only baseline, since that text is a snapshot rather than a tracked one.
48
- * - {@link invalidate} when the client rejects an edit (`applied=false`) or a doc is closed —
49
- * the shadow is now stale and the next sync must fall back to a full replace.
50
- */
51
- export declare class LanguageClientTextShadow {
52
- protected onFallback: (uri: string, reason: string) => void;
53
- protected documents: ShadowDocumentSource;
54
- protected shadow: Map<string, string>;
55
- /**
56
- * Per URI, the text the client declared it opened the document with, held
57
- * only until a real baseline exists. Consulted by {@link computeEdits} when
58
- * there is no shadow, and dropped on {@link invalidate}: a snapshot that
59
- * outlived what the client holds would let the equality check skip an edit
60
- * the client still needs.
61
- */
62
- protected opened: Map<string, string>;
63
- /**
64
- * @param onFallback invoked when {@link computeEdits} falls back to a full-range replace due to
65
- * apply-verify mismatch. Useful for logging regressions.
66
- * @param documents the {@link ShadowDocumentSource} used to materialise the throwaway probe
67
- * document in {@link computeEdits}. In production this is the owning
68
- * `HydraniumTextDocuments` (whose `create` threads through its configured
69
- * `TextDocumentsConfiguration`); tests pass a minimal stub. Routing
70
- * through the manager rather than `TextDocument.create` directly avoids the
71
- * `instanceof FullTextDocument` gate in the bare LSP `TextDocument.update`,
72
- * keeping the verifier honest for adopters with a custom text-document type.
73
- */
74
- constructor(onFallback: (uri: string, reason: string) => void, documents: ShadowDocumentSource);
75
- /** Record the text the language client is known to hold (e.g. after a `didChange` echo). */
76
- set(uri: string, text: string): void;
77
- /**
78
- * Record the buffer the language client declared at `didOpen`, for a URI
79
- * with no baseline yet.
80
- *
81
- * Deliberately NOT a {@link set}: this text is only ever compared for
82
- * EQUALITY, never diffed against. A line-keyed diff is position-dependent,
83
- * so keying one to a `didOpen` snapshot the client may have moved past
84
- * splices its buffer — the failure {@link computeEdits}'s full-replace
85
- * fallback exists to avoid.
86
- */
87
- setOpenedText(uri: string, text: string): void;
88
- /**
89
- * The text the language client is believed to hold: the tracked text, else
90
- * the buffer it declared at `didOpen`.
91
- *
92
- * Read at the `workspace/applyEdit` egress BEFORE {@link computeEdits},
93
- * which overwrites the tracked text and drops the opened snapshot. A change
94
- * the client sends carries ranges addressing this text and nothing else, so
95
- * losing it leaves that change unreconstructable — and applying its ranges
96
- * to the already-advanced synced text splices lines they never addressed.
97
- *
98
- * Deliberately wider than what {@link computeEdits} may diff from: an
99
- * opened snapshot is a position-dependent key to a buffer the client may
100
- * have moved past, so keying a diff to it splices that buffer.
101
- */
102
- clientText(uri: string): string | undefined;
103
- /**
104
- * Whether a tracked text exists for `uri`, which is what decides whether a
105
- * push can be diffed at all. Distinct from {@link clientText}, which also
106
- * answers from the opened snapshot and so cannot settle this.
107
- */
108
- isTracked(uri: string): boolean;
109
- /** Forget the shadow for a URI; next {@link computeEdits} falls back to a full-document replace. */
110
- invalidate(uri: string): void;
111
- /**
112
- * Return the minimal LSP {@link TextEdit}s required to bring the language client's content from
113
- * the tracked shadow to `newText`. Updates the shadow to `newText` so subsequent calls diff
114
- * against it.
115
- *
116
- * - Empty array when the shadow already equals `newText` (caller should skip the RPC),
117
- * or when there is no shadow and the client opened the document with exactly `newText`
118
- * (see {@link setOpenedText}) — pushing it anyway dirties the client's buffer on open.
119
- * - Full-range replace when there is no shadow (first sync or post-{@link invalidate}).
120
- * - Line-level diff otherwise. Before returning, we locally reconstruct `newText` by applying
121
- * the diff to `old`; if that doesn't match, we fall back to a full-range replace and notify
122
- * the fallback callback. This catches any algorithmic regression in {@link diffToEdits}
123
- * without risking client corruption.
124
- */
125
- computeEdits(uri: string, newText: string): TextEdit[];
126
- }
127
- /**
128
- * Convert a {@link diffLines} result to a list of LSP {@link TextEdit}s keyed on line positions
129
- * in the *old* text. Contiguous added/removed hunks are coalesced into a single replace so the
130
- * client sees one edit per changed region.
131
- */
132
- export declare function diffToEdits(oldText: string, newText: string): TextEdit[];
133
- //# sourceMappingURL=language-client-text-shadow.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"language-client-text-shadow.d.ts","sourceRoot":"","sources":["../../src/documents/language-client-text-shadow.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAGlF,OAAO,EAAS,KAAK,QAAQ,EAAY,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAElE;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IAClC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CAAC;CAC1F;AAKD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,OAAO,CAMjE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,wBAAwB;IAwB/B,SAAS,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI;IAC3D,SAAS,CAAC,SAAS,EAAE,oBAAoB;IAxB5C,SAAS,CAAC,MAAM,sBAA6B;IAE7C;;;;;;OAMG;IACH,SAAS,CAAC,MAAM,sBAA6B;IAE7C;;;;;;;;;;OAUG;IACH,YACa,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,EACjD,SAAS,EAAE,oBAAoB,EACxC;IAEJ,4FAA4F;IAC5F,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAEnC;IAED;;;;;;;;;OASG;IACH,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE7C;IAED;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE1C;IAED;;;;OAIG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9B;IAED,oGAAoG;IACpG,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAO5B;IAED;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,EAAE,CAqBrD;CACH;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,EAAE,CA+BxE"}