@hydranium/core 1.0.0-next.271 → 1.0.0-next.273

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 (79) hide show
  1. package/lib/documents/ast-document-manager.d.ts +37 -4
  2. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  3. package/lib/documents/ast-document-manager.js +54 -6
  4. package/lib/documents/ast-document-manager.js.map +1 -1
  5. package/lib/documents/dirty-state-tracker.d.ts +15 -0
  6. package/lib/documents/dirty-state-tracker.d.ts.map +1 -1
  7. package/lib/documents/dirty-state-tracker.js +14 -1
  8. package/lib/documents/dirty-state-tracker.js.map +1 -1
  9. package/lib/documents/hydranium-text-documents.d.ts +14 -0
  10. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  11. package/lib/documents/hydranium-text-documents.js +22 -0
  12. package/lib/documents/hydranium-text-documents.js.map +1 -1
  13. package/lib/langium/model-service/client-session.d.ts +35 -6
  14. package/lib/langium/model-service/client-session.d.ts.map +1 -1
  15. package/lib/langium/model-service/client-session.js +20 -2
  16. package/lib/langium/model-service/client-session.js.map +1 -1
  17. package/lib/langium/model-service/model-events.d.ts +5 -0
  18. package/lib/langium/model-service/model-events.d.ts.map +1 -1
  19. package/lib/langium/model-service/model-service.d.ts +7 -0
  20. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  21. package/lib/langium/model-service/model-service.js +1 -1
  22. package/lib/langium/model-service/model-service.js.map +1 -1
  23. package/lib/langium/workspace/file-system-provider.d.ts +13 -5
  24. package/lib/langium/workspace/file-system-provider.d.ts.map +1 -1
  25. package/lib/langium/workspace/file-system-provider.js +11 -5
  26. package/lib/langium/workspace/file-system-provider.js.map +1 -1
  27. package/lib/langium/workspace/in-memory-file-system-provider.d.ts +4 -2
  28. package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
  29. package/lib/langium/workspace/in-memory-file-system-provider.js +9 -3
  30. package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
  31. package/lib/langium/workspace/persistent-file-system-provider.d.ts +6 -1
  32. package/lib/langium/workspace/persistent-file-system-provider.d.ts.map +1 -1
  33. package/lib/langium/workspace/persistent-file-system-provider.js +12 -5
  34. package/lib/langium/workspace/persistent-file-system-provider.js.map +1 -1
  35. package/lib/langium/workspace/virtual-document.d.ts +2 -1
  36. package/lib/langium/workspace/virtual-document.d.ts.map +1 -1
  37. package/lib/langium/workspace/virtual-document.js +5 -2
  38. package/lib/langium/workspace/virtual-document.js.map +1 -1
  39. package/lib/langium/workspace/write-lock-scope.d.ts +4 -2
  40. package/lib/langium/workspace/write-lock-scope.d.ts.map +1 -1
  41. package/lib/langium/workspace/write-lock-scope.js +6 -4
  42. package/lib/langium/workspace/write-lock-scope.js.map +1 -1
  43. package/lib/lsp/hydranium-document-update-handler.d.ts +14 -1
  44. package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
  45. package/lib/lsp/hydranium-document-update-handler.js +27 -1
  46. package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
  47. package/lib/messages/index.d.ts +1 -1
  48. package/lib/messages/index.d.ts.map +1 -1
  49. package/lib/messages/index.js +1 -1
  50. package/lib/messages/index.js.map +1 -1
  51. package/lib/node/node-file-system-provider.d.ts +6 -0
  52. package/lib/node/node-file-system-provider.d.ts.map +1 -1
  53. package/lib/node/node-file-system-provider.js +22 -1
  54. package/lib/node/node-file-system-provider.js.map +1 -1
  55. package/lib/testing/stub-ast-document-manager.d.ts +1 -1
  56. package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
  57. package/lib/testing/stub-ast-document-manager.js +10 -0
  58. package/lib/testing/stub-ast-document-manager.js.map +1 -1
  59. package/lib/testing/stub-hydranium-text-documents.d.ts +1 -1
  60. package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
  61. package/lib/testing/stub-hydranium-text-documents.js +17 -0
  62. package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
  63. package/package.json +5 -5
  64. package/src/documents/ast-document-manager.ts +86 -6
  65. package/src/documents/dirty-state-tracker.ts +29 -2
  66. package/src/documents/hydranium-text-documents.ts +24 -0
  67. package/src/langium/model-service/client-session.ts +48 -7
  68. package/src/langium/model-service/model-events.ts +5 -0
  69. package/src/langium/model-service/model-service.ts +8 -1
  70. package/src/langium/workspace/file-system-provider.ts +19 -5
  71. package/src/langium/workspace/in-memory-file-system-provider.ts +13 -3
  72. package/src/langium/workspace/persistent-file-system-provider.ts +12 -5
  73. package/src/langium/workspace/virtual-document.ts +6 -2
  74. package/src/langium/workspace/write-lock-scope.ts +6 -4
  75. package/src/lsp/hydranium-document-update-handler.ts +30 -1
  76. package/src/messages/index.ts +1 -1
  77. package/src/node/node-file-system-provider.ts +22 -1
  78. package/src/testing/stub-ast-document-manager.ts +22 -1
  79. package/src/testing/stub-hydranium-text-documents.ts +19 -0
@@ -167,7 +167,7 @@ export class PersistentFileSystemProvider extends InMemoryFileSystemProvider {
167
167
  // insertion order of a merged record.
168
168
  const uri = this.storeUri(path);
169
169
  if (content === TOMBSTONE) {
170
- super.deleteFile(uri);
170
+ this.files.delete(this.fileKey(uri));
171
171
  } else {
172
172
  super.setFile(uri, content);
173
173
  }
@@ -202,9 +202,16 @@ export class PersistentFileSystemProvider extends InMemoryFileSystemProvider {
202
202
  await this.persist(uri, content);
203
203
  }
204
204
 
205
- override deleteFile(uri: URI): void {
206
- super.deleteFile(uri);
207
- this.forget(uri).catch((error: unknown) => this.reportStoreFailure('forget', uri, error));
205
+ /**
206
+ * Mirror the deletion into the store and REJECT if it refuses, as
207
+ * {@link writeFile} does. The store goes first, so one that refuses leaves
208
+ * the file it still holds; a directory is refused before the store is asked.
209
+ */
210
+ override async deleteFile(uri: URI): Promise<void> {
211
+ if (!this.hasChildren(this.fileKey(uri))) {
212
+ await this.forget(uri);
213
+ }
214
+ await super.deleteFile(uri);
208
215
  }
209
216
 
210
217
  /** The absolute URI a store-space `path` addresses. */
@@ -245,7 +252,7 @@ export class PersistentFileSystemProvider extends InMemoryFileSystemProvider {
245
252
  return key;
246
253
  }
247
254
  if (!key.startsWith(this.storeRoot)) {
248
- throw new Error(`Cannot persist ${uri.toString()}: outside the store root ${this.storeRoot}`);
255
+ throw new Error(`${uri.toString()} is outside the store root ${this.storeRoot}`);
249
256
  }
250
257
  return key.slice(this.storeRoot.length);
251
258
  }
@@ -12,7 +12,7 @@ import { type Tracer } from '@hydranium/protocol';
12
12
  import { type WritableFileSystemProvider } from './file-system-provider.js';
13
13
  import { type LogNameOptions } from '../diagnostics/logger.js';
14
14
  import { type ServerSharedServicesMinimal } from '../shared-services.js';
15
- import { NO_SUCH_FILE, NO_SUCH_PATH, UNSUPPORTED_WRITE } from './in-memory-file-system-provider.js';
15
+ import { NO_SUCH_FILE, NO_SUCH_PATH, UNSUPPORTED_DELETE, UNSUPPORTED_WRITE } from './in-memory-file-system-provider.js';
16
16
  import { notFound } from './file-not-found.js';
17
17
 
18
18
  /**
@@ -95,7 +95,7 @@ export function isVirtualUri(uri: URI | string): boolean {
95
95
  * always a FILE with no children: a workspace walk handed a directory would try
96
96
  * to enumerate children it has none of.
97
97
  *
98
- * Read-only: a write is refused, since nothing backs the scheme.
98
+ * Read-only: a write or delete is refused, since nothing backs the scheme.
99
99
  */
100
100
  export class VirtualFileSystemProvider implements WritableFileSystemProvider {
101
101
  protected readonly tracer: Tracer;
@@ -111,6 +111,10 @@ export class VirtualFileSystemProvider implements WritableFileSystemProvider {
111
111
  throw new Error(UNSUPPORTED_WRITE.format({ uri: uri.toString() }));
112
112
  }
113
113
 
114
+ async deleteFile(uri: URI): Promise<void> {
115
+ throw new Error(UNSUPPORTED_DELETE.format({ uri: uri.toString() }));
116
+ }
117
+
114
118
  async readFile(uri: URI): Promise<string> {
115
119
  return this.readFileSync(uri);
116
120
  }
@@ -79,22 +79,24 @@ export function isWriteLockScopeInstalled(): boolean {
79
79
  /**
80
80
  * Thrown when the model facade is reached from inside a workspace write-lock
81
81
  * holder — typically an integrity rule or build-phase pass writing through
82
- * a session's `update` / `save` or `ModelService.rebuild` during a build.
82
+ * a session's `update` / `save` / `delete` or `ModelService.rebuild` during a
83
+ * build.
83
84
  *
84
85
  * Named rather than a bare `Error` because the remedy is specific and worth
85
86
  * pointing at: either move the write out of the build (integrity rules that
86
87
  * persist repairs should write through `FileSystemProvider.writeFile`, which
87
88
  * takes no lock), or set `ModelServiceOptions.allowReentrantBuilds: true`,
88
89
  * which keeps the facade's build from cancelling the holder but not its wait
89
- * from stalling on a document that needs a re-queue.
90
+ * from stalling on a document that needs a re-queue. A delete is refused on
91
+ * either value.
90
92
  */
91
93
  export class ReentrantWriteLockError extends Error {
92
94
  constructor(readonly uri: string) {
93
95
  super(
94
96
  `Model facade reached for '${uri}' from inside a workspace write-lock holder. ` +
95
97
  'Acquiring the write lock would cancel the enclosing build and may stall this call. ' +
96
- 'Write through FileSystemProvider.writeFile from a build-phase pass, or set ' +
97
- 'ModelServiceOptions.allowReentrantBuilds: true to build without the lock from inside it.'
98
+ 'Write through FileSystemProvider.writeFile from a build-phase pass, or, for an update, save or rebuild, ' +
99
+ 'set ModelServiceOptions.allowReentrantBuilds: true to build without the lock from inside it.'
98
100
  );
99
101
  this.name = 'ReentrantWriteLockError';
100
102
  }
@@ -80,7 +80,8 @@ export interface HydraniumDocumentUpdateHandlerOptions {
80
80
  *
81
81
  * 1. **Self-save filter** ({@link filterSelfSaves}) — drops
82
82
  * watched-file-change echoes for the server's own writes via
83
- * {@link SelfSaveRegistry}, keyed by `fsPath` + mtime.
83
+ * {@link SelfSaveRegistry}, keyed by `fsPath` + mtime, and
84
+ * ({@link filterStaleDeletions}) deletions the disk contradicts.
84
85
  * 2. **Trailing-edge debouncing** — collapses rapid
85
86
  * `didChangeContent` calls into one rebuild on the trailing
86
87
  * edge of the configured window. Non-language-client author
@@ -218,6 +219,7 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
218
219
 
219
220
  override didChangeWatchedFiles(params: DidChangeWatchedFilesParams): void {
220
221
  void this.filterSelfSaves(params)
222
+ .then(unsaved => this.filterStaleDeletions(unsaved))
221
223
  .then(filtered => {
222
224
  if (filtered.changes.length === 0) {
223
225
  return;
@@ -348,6 +350,33 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
348
350
  return { changes: kept.filter((change): change is FileEvent => change !== undefined) };
349
351
  }
350
352
 
353
+ /**
354
+ * Drop a deletion the disk contradicts: the file is there, or the document
355
+ * at the URI was created from a session's text and its file not seen since,
356
+ * as one created again after a delete. A watcher reports a delete late, the
357
+ * server's own included, and applied to a document created again since, it
358
+ * would close every open of it. A missing disk baseline cannot stand in for
359
+ * the creation: a report that finds the file gone clears it too. A record
360
+ * of the server's own unlinks could not tell its echo from a later real
361
+ * delete, since a delete leaves no mtime to match. A miss the provider
362
+ * reports in error keeps the deletion.
363
+ */
364
+ protected async filterStaleDeletions(params: DidChangeWatchedFilesParams): Promise<DidChangeWatchedFilesParams> {
365
+ const kept = await Promise.all(
366
+ params.changes.map(async change => {
367
+ if (change.type !== FileChangeType.Deleted) {
368
+ return change;
369
+ }
370
+ if (this.textDocuments.isUnsavedCreation(change.uri)) {
371
+ return undefined;
372
+ }
373
+ const exists = await this.fileSystemProvider.exists(UriUtils.toUri(change.uri)).catch(() => false);
374
+ return exists ? undefined : change;
375
+ })
376
+ );
377
+ return { changes: kept.filter((change): change is FileEvent => change !== undefined) };
378
+ }
379
+
351
380
  /**
352
381
  * Coalesce `changed` / `deleted` into the pending sets, then either
353
382
  * flush immediately (`immediateFlush` or `debounceMs <= 0`) or restart
@@ -33,4 +33,4 @@ export {
33
33
  UNRESOLVED_REFERENCE
34
34
  } from '../langium/validation/document-validator.js';
35
35
  export { NO_LOADABLE_CONTENT } from '../langium/workspace/langium-documents.js';
36
- export { NO_SUCH_FILE, NO_SUCH_PATH, UNSUPPORTED_WRITE } from '../langium/workspace/in-memory-file-system-provider.js';
36
+ export { NO_SUCH_FILE, NO_SUCH_PATH, UNSUPPORTED_DELETE, UNSUPPORTED_WRITE } from '../langium/workspace/in-memory-file-system-provider.js';
@@ -14,11 +14,12 @@ import { randomUUID } from 'node:crypto';
14
14
  import { realpathSync } from 'node:fs';
15
15
  import * as fsp from 'node:fs/promises';
16
16
  import { type WritableFileSystemProvider } from '../langium/workspace/file-system-provider.js';
17
+ import { isFileNotFound } from '../langium/workspace/file-not-found.js';
17
18
  import { type SelfSaveRegistry } from '../langium/workspace/self-save-registry.js';
18
19
  import { RENAME_GAP_BUDGET_MS, renameOverOpenReaders } from './rename-over-open-readers.js';
19
20
  import { type LogNameOptions } from '../langium/diagnostics/logger.js';
20
21
  import { serverSharedFactory, type ServerSharedServicesMinimal } from '../langium/shared-services.js';
21
- import { NO_SUCH_FILE, NO_SUCH_PATH, UNSUPPORTED_WRITE } from '../langium/workspace/in-memory-file-system-provider.js';
22
+ import { NO_SUCH_FILE, NO_SUCH_PATH, UNSUPPORTED_DELETE, UNSUPPORTED_WRITE } from '../langium/workspace/in-memory-file-system-provider.js';
22
23
 
23
24
  /**
24
25
  * Node-based default {@link WritableFileSystemProvider}. Extends Langium's
@@ -252,6 +253,26 @@ export class DefaultFileSystemProvider extends NodeFileSystemProvider implements
252
253
  }
253
254
  }
254
255
 
256
+ /**
257
+ * Unlink `uri`, which removes a symlink rather than the file it points at
258
+ * and leaves a parent directory it empties in place. A file already gone is
259
+ * no error; a directory is refused, as `unlink` refuses it.
260
+ */
261
+ async deleteFile(uri: URI): Promise<void> {
262
+ // Before the path is derived, for the reason `writeFile` gives: `fsPath`
263
+ // would name an unrelated file for a URI naming no disk location.
264
+ if (!this.servesFromDisk(uri)) {
265
+ throw new Error(UNSUPPORTED_DELETE.format({ uri: uri.toString() }));
266
+ }
267
+ try {
268
+ await fsp.unlink(uri.fsPath);
269
+ } catch (error: unknown) {
270
+ if (!isFileNotFound(error, uri)) {
271
+ throw error;
272
+ }
273
+ }
274
+ }
275
+
255
276
  async mtimeMs(uri: URI): Promise<number | undefined> {
256
277
  // Same rule as the read surface, and "unknown" is already the answer the
257
278
  // caller handles — so a URI naming no disk location reports no mtime
@@ -41,7 +41,18 @@ import type { StubLangiumDocuments } from './stub-langium-documents.js';
41
41
  */
42
42
  export interface StubAstDocumentManager<TAst extends AstNode, TDiagnostic extends AstDiagnostic = AstDiagnostic> extends Pick<
43
43
  AstDocumentManager<TAst, TDiagnostic>,
44
- 'open' | 'close' | 'isOpen' | 'update' | 'save' | 'getAuthor' | 'getDocument' | 'toAstDocument' | 'attributeUpdate'
44
+ | 'open'
45
+ | 'close'
46
+ | 'isOpen'
47
+ | 'update'
48
+ | 'save'
49
+ | 'delete'
50
+ | 'isDeleting'
51
+ | 'attributeDeletion'
52
+ | 'getAuthor'
53
+ | 'getDocument'
54
+ | 'toAstDocument'
55
+ | 'attributeUpdate'
45
56
  > {
46
57
  readonly openClients: Map<string, Set<string>>;
47
58
  }
@@ -96,6 +107,16 @@ export function makeStubAstDocumentManager<TAst extends AstNode, TDiagnostic ext
96
107
  textDocuments.notifyDidSaveTextDocument({ textDocument: { uri }, text }, clientId);
97
108
  return version;
98
109
  },
110
+ delete(): Promise<void> {
111
+ return notSupported('delete');
112
+ },
113
+ // The stub deletes nothing, so no delete is ever under way.
114
+ isDeleting(): boolean {
115
+ return false;
116
+ },
117
+ attributeDeletion(): string {
118
+ return UNKNOWN_CLIENT_ID;
119
+ },
99
120
  getAuthor(): string {
100
121
  return notSupported('getAuthor');
101
122
  },
@@ -92,6 +92,8 @@ export interface StubHydraniumTextDocuments extends Pick<
92
92
  | 'openedVersion'
93
93
  | 'onDidChangeDirty'
94
94
  | 'setDiskBaseline'
95
+ | 'markCreated'
96
+ | 'isUnsavedCreation'
95
97
  > {
96
98
  /**
97
99
  * Stub-tailored read accessor. Returns just the surface the framework's
@@ -173,6 +175,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
173
175
  const key = (uri: string): CanonicalUri => uri as CanonicalUri;
174
176
  const documentReleased = new Emitter<DocumentReleasedEvent>();
175
177
  const baselines = new Map<string, string | undefined>();
178
+ const created = new Set<string>();
176
179
  const openedVersions = new Map<string, number>();
177
180
  const dirty = new Set<string>();
178
181
  const dirtyChanged = new Emitter<DocumentDirtyChangedEvent>();
@@ -329,9 +332,22 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
329
332
  setDiskBaseline(uri, text) {
330
333
  if (docs.has(uri)) {
331
334
  baselines.set(uri, text);
335
+ if (text !== undefined) {
336
+ created.delete(uri);
337
+ }
338
+ refreshDirty(uri);
339
+ }
340
+ },
341
+ markCreated(uri) {
342
+ if (docs.has(uri)) {
343
+ baselines.set(uri, undefined);
344
+ created.add(uri);
332
345
  refreshDirty(uri);
333
346
  }
334
347
  },
348
+ isUnsavedCreation(uri) {
349
+ return created.has(uri);
350
+ },
335
351
  openDocuments() {
336
352
  // From the registry that also answers `isOpenInClient`, which both
337
353
  // seeding channels feed, so the two never disagree about an open.
@@ -355,6 +371,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
355
371
  docs.set(uri, { uri, version: 1, text, clientId });
356
372
  openedVersions.set(uri, 1);
357
373
  baselines.set(uri, text);
374
+ created.delete(uri);
358
375
  },
359
376
  seedOpenInLanguageClient(uri) {
360
377
  sessions.addOpen(key(uri), LANGUAGE_CLIENT_ID);
@@ -368,6 +385,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
368
385
  const dropped = holder !== undefined && holder.clientId === clientId;
369
386
  if (dropped) {
370
387
  docs.delete(uri);
388
+ created.delete(uri);
371
389
  }
372
390
  const removed = sessions.removeOpen(key(uri), clientId);
373
391
  if (clientId === LANGUAGE_CLIENT_ID) {
@@ -393,6 +411,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
393
411
  appliedEdits.length = 0;
394
412
  staged.length = 0;
395
413
  baselines.clear();
414
+ created.clear();
396
415
  openedVersions.clear();
397
416
  dirty.clear();
398
417
  saveListeners.length = 0;