@hydranium/core 1.0.0-next.155 → 1.0.0-next.157

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 (55) hide show
  1. package/lib/documents/ast-document-manager.d.ts +3 -2
  2. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  3. package/lib/documents/ast-document-manager.js.map +1 -1
  4. package/lib/documents/hydranium-text-documents.d.ts +3 -2
  5. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  6. package/lib/documents/hydranium-text-documents.js +3 -2
  7. package/lib/documents/hydranium-text-documents.js.map +1 -1
  8. package/lib/langium/module.d.ts +38 -12
  9. package/lib/langium/module.d.ts.map +1 -1
  10. package/lib/langium/module.js +15 -22
  11. package/lib/langium/module.js.map +1 -1
  12. package/lib/langium/workspace/document-uri-policy.d.ts +7 -5
  13. package/lib/langium/workspace/document-uri-policy.d.ts.map +1 -1
  14. package/lib/langium/workspace/document-uri-policy.js +7 -5
  15. package/lib/langium/workspace/document-uri-policy.js.map +1 -1
  16. package/lib/langium/workspace/file-not-found.d.ts +17 -0
  17. package/lib/langium/workspace/file-not-found.d.ts.map +1 -0
  18. package/lib/langium/workspace/file-not-found.js +20 -0
  19. package/lib/langium/workspace/file-not-found.js.map +1 -0
  20. package/lib/langium/workspace/file-system-provider.d.ts +81 -13
  21. package/lib/langium/workspace/file-system-provider.d.ts.map +1 -1
  22. package/lib/langium/workspace/file-system-provider.js +81 -35
  23. package/lib/langium/workspace/file-system-provider.js.map +1 -1
  24. package/lib/langium/workspace/hydranium-workspace-manager.d.ts +51 -2
  25. package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
  26. package/lib/langium/workspace/hydranium-workspace-manager.js +82 -4
  27. package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
  28. package/lib/langium/workspace/in-memory-file-system-provider.d.ts +2 -2
  29. package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
  30. package/lib/langium/workspace/in-memory-file-system-provider.js +3 -23
  31. package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
  32. package/lib/langium/workspace/persistent-file-system-provider.d.ts +0 -7
  33. package/lib/langium/workspace/persistent-file-system-provider.d.ts.map +1 -1
  34. package/lib/langium/workspace/persistent-file-system-provider.js +0 -7
  35. package/lib/langium/workspace/persistent-file-system-provider.js.map +1 -1
  36. package/lib/langium/workspace/virtual-document.d.ts +44 -26
  37. package/lib/langium/workspace/virtual-document.d.ts.map +1 -1
  38. package/lib/langium/workspace/virtual-document.js +76 -33
  39. package/lib/langium/workspace/virtual-document.js.map +1 -1
  40. package/lib/node/node-file-system-provider.d.ts +10 -9
  41. package/lib/node/node-file-system-provider.d.ts.map +1 -1
  42. package/lib/node/node-file-system-provider.js +12 -47
  43. package/lib/node/node-file-system-provider.js.map +1 -1
  44. package/package.json +5 -5
  45. package/src/documents/ast-document-manager.ts +3 -2
  46. package/src/documents/hydranium-text-documents.ts +3 -2
  47. package/src/langium/module.ts +56 -34
  48. package/src/langium/workspace/document-uri-policy.ts +7 -5
  49. package/src/langium/workspace/file-not-found.ts +23 -0
  50. package/src/langium/workspace/file-system-provider.ts +136 -39
  51. package/src/langium/workspace/hydranium-workspace-manager.ts +98 -6
  52. package/src/langium/workspace/in-memory-file-system-provider.ts +3 -24
  53. package/src/langium/workspace/persistent-file-system-provider.ts +0 -7
  54. package/src/langium/workspace/virtual-document.ts +92 -33
  55. package/src/node/node-file-system-provider.ts +12 -48
@@ -12,7 +12,7 @@ import { defineMessage, type Tracer } from '@hydranium/protocol';
12
12
  import { type WritableFileSystemProvider } from '../../documents/ast-document-manager.js';
13
13
  import { type LogNameOptions } from '../diagnostics/logger.js';
14
14
  import { serverSharedFactory, type ServerSharedServicesMinimal } from '../shared-services.js';
15
- import { serveVirtualDocument, serveVirtualNode } from './virtual-document.js';
15
+ import { notFound } from './file-not-found.js';
16
16
 
17
17
  /**
18
18
  * A provider's spelling of "the document is not there", raised wherever a read
@@ -43,15 +43,6 @@ export const UNSUPPORTED_WRITE = defineMessage(
43
43
  'Cannot write {uri}: the scheme names no location on disk'
44
44
  );
45
45
 
46
- /**
47
- * A missing-file error shaped as a Node file system raises it, with code
48
- * `ENOENT` and the path: callers tell a missing file from any other failure by
49
- * those two fields, whichever provider is bound.
50
- */
51
- function notFound(message: string, uri: URI): Error {
52
- return Object.assign(new Error(message), { code: 'ENOENT', path: uri.fsPath });
53
- }
54
-
55
46
  /**
56
47
  * File content keyed by path, for seeding an {@link InMemoryFileSystemProvider}.
57
48
  *
@@ -165,10 +156,6 @@ export class InMemoryFileSystemProvider implements WritableFileSystemProvider {
165
156
  }
166
157
 
167
158
  readFileSync(uri: URI): string {
168
- const served = serveVirtualDocument(this.services, uri);
169
- if (served !== undefined) {
170
- return served;
171
- }
172
159
  const content = this.files.get(normalize(uri));
173
160
  if (content === undefined) {
174
161
  throw notFound(NO_SUCH_FILE.format({ uri: uri.toString() }), uri);
@@ -189,11 +176,6 @@ export class InMemoryFileSystemProvider implements WritableFileSystemProvider {
189
176
  }
190
177
 
191
178
  statSync(uri: URI): FileSystemNode {
192
- // Virtual first, on the same terms as `readFileSync`.
193
- const served = serveVirtualNode(this.services, uri);
194
- if (served !== undefined) {
195
- return served;
196
- }
197
179
  const path = normalize(uri);
198
180
  if (this.files.has(path)) {
199
181
  return { isFile: true, isDirectory: false, uri };
@@ -209,9 +191,6 @@ export class InMemoryFileSystemProvider implements WritableFileSystemProvider {
209
191
  }
210
192
 
211
193
  existsSync(uri: URI): boolean {
212
- if (serveVirtualDocument(this.services, uri) !== undefined) {
213
- return true;
214
- }
215
194
  const path = normalize(uri);
216
195
  return this.files.has(path) || this.hasChildren(path);
217
196
  }
@@ -281,8 +260,8 @@ function normalize(uri: URI | string): string {
281
260
  * services context exactly where a Node host spreads `NodeFileSystem`.
282
261
  *
283
262
  * A host that needs to reach the provider afterwards — to seed or mutate it
284
- * mid-test — reads it back off `shared.workspace.FileSystemProvider`, since the
285
- * slot owns construction.
263
+ * mid-test — narrows `shared.workspace.FileSystemProvider.host` to this
264
+ * class, since the slot owns construction.
286
265
  */
287
266
  export function inMemoryFileSystem(options: InMemoryFileSystemOptions = {}): {
288
267
  fileSystemProvider: (services: unknown) => WritableFileSystemProvider;
@@ -125,13 +125,6 @@ export interface PersistentFileSystemOptions extends InMemoryFileSystemOptions {
125
125
  * restored as deleted, which is why the marker is NUL-wrapped and text a grammar
126
126
  * can parse is not.
127
127
  *
128
- * # A registered virtual document still wins
129
- *
130
- * `readFileSync` consults the virtual-document seam before the map, so a stdlib
131
- * contributed in code keeps answering for its URI whether or not the store holds
132
- * one. The store cannot shadow it, which is what keeps a rebuild that re-reads
133
- * that URI from losing the stdlib.
134
- *
135
128
  * # What it deliberately does not answer
136
129
  *
137
130
  * The store is scoped to the ORIGIN, not to the page: two pages on the same
@@ -8,14 +8,19 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  import { type FileSystemNode, URI } from '@hydranium/langium';
11
+ import { type Tracer } from '@hydranium/protocol';
12
+ import { type WritableFileSystemProvider } from '../../documents/ast-document-manager.js';
13
+ import { type LogNameOptions } from '../diagnostics/logger.js';
11
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';
16
+ import { notFound } from './file-not-found.js';
12
17
 
13
18
  /**
14
19
  * URI scheme for virtual documents — LangiumDocuments that have no backing
15
20
  * file on disk (a language's stdlib, library types, built-in definitions,
16
21
  * mirrors, generated views). Chosen so it cannot collide with on-disk `file:`
17
- * URIs, and so the framework's FileSystemProvider can recognise and serve them
18
- * on re-read (see {@link serveVirtualDocument}).
22
+ * URIs, and so the framework can serve them on re-read through its own
23
+ * provider for the scheme (see {@link VirtualFileSystemProvider}).
19
24
  */
20
25
  export const VIRTUAL_SCHEME = 'virtual';
21
26
 
@@ -72,41 +77,95 @@ export function isVirtualUri(uri: URI | string): boolean {
72
77
  }
73
78
 
74
79
  /**
75
- * Serve the text of a registered virtual document, or `undefined` when `uri`
76
- * is not virtual or no document is registered for it (so a FileSystemProvider
77
- * delegates to its real backing).
80
+ * The file system provider for the {@link VIRTUAL_SCHEME} scheme, registered by
81
+ * the framework in the shared `fileSystemProviders` group so every host serves
82
+ * `virtual:` whatever provider it binds for its own files.
78
83
  *
79
- * The framework's FileSystemProvider defaults consult this across their whole
80
- * read surface, so a virtual document survives a re-read:
81
- * `DocumentBuilder.update` / `LangiumDocumentFactory.update` re-read a changed
82
- * URI from the FileSystemProvider, and nothing on disk backs a virtual URI. The
83
- * text comes from the registered document — the original source for a
84
- * `fromString` document, or the serialized form retained by the framework's
85
- * `fromModel` (which fills in the text Langium leaves empty). No serializer is
86
- * consulted here; the text is already on the document.
84
+ * A virtual document has no backing outside the index, and a re-read still has
85
+ * to find it: `DocumentBuilder.update` and `LangiumDocumentFactory.update`
86
+ * re-read a changed URI from the provider, and the last close rebuilds a
87
+ * released document the provider says `exists`. So the text comes from the
88
+ * document `LangiumDocuments` holds — the original source for a `fromString`
89
+ * document, or the serialized form the framework's `fromModel` retains. An
90
+ * edited one answers with its edited text.
87
91
  *
88
- * **Every read method has to agree.** Serving only the text methods leaves one
92
+ * **Every read method agrees.** Serving only the text methods would leave one
89
93
  * provider answering "here is the content" and "nothing is there" about one
90
- * URI, which is a worse contract than answering neither: a caller cannot probe
91
- * before reading.
94
+ * URI, so a caller could not probe before reading. A registered document is
95
+ * always a FILE with no children: a workspace walk handed a directory would try
96
+ * to enumerate children it has none of.
97
+ *
98
+ * Read-only: a write is refused, since nothing backs the scheme.
92
99
  */
93
- export function serveVirtualDocument(services: ServerSharedServicesMinimal, uri: URI): string | undefined {
94
- if (!isVirtualUri(uri)) {
95
- return undefined;
100
+ export class VirtualFileSystemProvider implements WritableFileSystemProvider {
101
+ protected readonly tracer: Tracer;
102
+
103
+ constructor(
104
+ protected readonly services: ServerSharedServicesMinimal,
105
+ options: LogNameOptions = {}
106
+ ) {
107
+ this.tracer = services.Tracer.for(options.logName ?? this.constructor.name).trace('instantiated');
96
108
  }
97
- const document = services.workspace.LangiumDocuments.getDocument(uri);
98
- return document ? document.textDocument.getText() : undefined;
99
- }
100
109
 
101
- /**
102
- * The filesystem node a registered virtual document presents as, or `undefined`
103
- * on the same terms as {@link serveVirtualDocument}.
104
- *
105
- * Always a FILE: a virtual document is a leaf carrying text, and nothing is
106
- * addressable beneath it. Reporting it as a directory instead would put it in
107
- * front of a workspace walk that would then try to enumerate children it has
108
- * none of.
109
- */
110
- export function serveVirtualNode(services: ServerSharedServicesMinimal, uri: URI): FileSystemNode | undefined {
111
- return serveVirtualDocument(services, uri) === undefined ? undefined : { isFile: true, isDirectory: false, uri };
110
+ async writeFile(uri: URI, _content: string): Promise<void> {
111
+ throw new Error(UNSUPPORTED_WRITE.format({ uri: uri.toString() }));
112
+ }
113
+
114
+ async readFile(uri: URI): Promise<string> {
115
+ return this.readFileSync(uri);
116
+ }
117
+
118
+ readFileSync(uri: URI): string {
119
+ const text = this.text(uri);
120
+ if (text === undefined) {
121
+ throw notFound(NO_SUCH_FILE.format({ uri: uri.toString() }), uri);
122
+ }
123
+ return text;
124
+ }
125
+
126
+ async readBinary(uri: URI): Promise<Uint8Array> {
127
+ return this.readBinarySync(uri);
128
+ }
129
+
130
+ readBinarySync(uri: URI): Uint8Array {
131
+ return new TextEncoder().encode(this.readFileSync(uri));
132
+ }
133
+
134
+ async stat(uri: URI): Promise<FileSystemNode> {
135
+ return this.statSync(uri);
136
+ }
137
+
138
+ statSync(uri: URI): FileSystemNode {
139
+ if (this.text(uri) === undefined) {
140
+ throw notFound(NO_SUCH_PATH.format({ uri: uri.toString() }), uri);
141
+ }
142
+ return { isFile: true, isDirectory: false, uri };
143
+ }
144
+
145
+ async exists(uri: URI): Promise<boolean> {
146
+ return this.existsSync(uri);
147
+ }
148
+
149
+ existsSync(uri: URI): boolean {
150
+ return this.text(uri) !== undefined;
151
+ }
152
+
153
+ async readDirectory(_uri: URI): Promise<FileSystemNode[]> {
154
+ return [];
155
+ }
156
+
157
+ readDirectorySync(_uri: URI): FileSystemNode[] {
158
+ return [];
159
+ }
160
+
161
+ /**
162
+ * Text of the document registered at `uri`, if there is one.
163
+ *
164
+ * `LangiumDocuments` is resolved per call, not in the constructor: it reaches
165
+ * this provider back through the document factory, so resolving it eagerly
166
+ * is a construction cycle.
167
+ */
168
+ protected text(uri: URI): string | undefined {
169
+ return this.services.workspace.LangiumDocuments.getDocument(uri)?.textDocument.getText();
170
+ }
112
171
  }
@@ -19,7 +19,6 @@ import { renameOverOpenReaders } from './rename-over-open-readers.js';
19
19
  import { type LogNameOptions } from '../langium/diagnostics/logger.js';
20
20
  import { serverSharedFactory, type ServerSharedServicesMinimal } from '../langium/shared-services.js';
21
21
  import { NO_SUCH_FILE, NO_SUCH_PATH, UNSUPPORTED_WRITE } from '../langium/workspace/in-memory-file-system-provider.js';
22
- import { serveVirtualDocument, serveVirtualNode } from '../langium/workspace/virtual-document.js';
23
22
 
24
23
  /**
25
24
  * Node-based default {@link WritableFileSystemProvider}. Extends Langium's
@@ -29,17 +28,18 @@ import { serveVirtualDocument, serveVirtualNode } from '../langium/workspace/vir
29
28
  * {@link SelfSaveRegistry} bound at `services.workspace.SelfSaveRegistry` — so
30
29
  * consumers can suppress the `didChangeWatchedFiles` echo for their own writes.
31
30
  *
32
- * Its read surface answers for registered virtual documents and refuses to
33
- * derive a disk path from a URI that names no disk location — see
34
- * {@link servesFromDisk}, which is the one place that judgement is made. Every
35
- * method that derives an OS path is held to it, writes most of all: a read that
36
- * skipped it would only mis-report, while a write would CREATE a file at a path
37
- * the URI never named.
31
+ * Its read surface refuses to derive a disk path from a URI that names no disk
32
+ * location — see {@link servesFromDisk}, which is the one place that judgement
33
+ * is made. Every method that derives an OS path is held to it, writes most of
34
+ * all: a read that skipped it would only mis-report, while a write would CREATE
35
+ * a file at a path the URI never named. Behind the framework's
36
+ * `FileSystemProviderRegistry`, a `virtual:` URI goes to the framework's own
37
+ * provider for that scheme, not here.
38
38
  *
39
39
  * Server-only (`@hydranium/core/node`): pulls `node:fs`. The portable `.`
40
- * entry binds `DefaultEmptyFileSystemProvider` by default, so a Node host
41
- * must rebind this slot — through `context.fileSystemProvider` or directly — to
42
- * get real disk I/O.
40
+ * entry falls back to `DefaultEmptyFileSystemProvider`, so a Node host passes
41
+ * this one through `context.fileSystemProvider` (see {@link NodeFileSystem})
42
+ * to get real disk I/O.
43
43
  */
44
44
  export class DefaultFileSystemProvider extends NodeFileSystemProvider implements WritableFileSystemProvider {
45
45
  readonly selfSaveRegistry: SelfSaveRegistry;
@@ -76,23 +76,14 @@ export class DefaultFileSystemProvider extends NodeFileSystemProvider implements
76
76
  return uri.scheme === 'file';
77
77
  }
78
78
 
79
- // Each read resolves in the same order: a registered virtual document first
80
- // (it has no `fsPath` backing), then disk for a path this provider can
81
- // reach, then the filesystem's own "nothing here" answer — never a probe
79
+ // Each read answers from disk for a path this provider can reach, and
80
+ // otherwise with the filesystem's own "nothing here" — never a probe
82
81
  // against a path derived from a URI that names no disk location.
83
82
  override readFile(uri: URI): Promise<string> {
84
- const served = serveVirtualDocument(this.services, uri);
85
- if (served !== undefined) {
86
- return Promise.resolve(served);
87
- }
88
83
  return this.servesFromDisk(uri) ? super.readFile(uri) : Promise.reject(noSuchFile(uri));
89
84
  }
90
85
 
91
86
  override readFileSync(uri: URI): string {
92
- const served = serveVirtualDocument(this.services, uri);
93
- if (served !== undefined) {
94
- return served;
95
- }
96
87
  if (!this.servesFromDisk(uri)) {
97
88
  throw noSuchFile(uri);
98
89
  }
@@ -100,18 +91,10 @@ export class DefaultFileSystemProvider extends NodeFileSystemProvider implements
100
91
  }
101
92
 
102
93
  override readBinary(uri: URI): Promise<Uint8Array> {
103
- const served = serveVirtualDocument(this.services, uri);
104
- if (served !== undefined) {
105
- return Promise.resolve(encode(served));
106
- }
107
94
  return this.servesFromDisk(uri) ? super.readBinary(uri) : Promise.reject(noSuchFile(uri));
108
95
  }
109
96
 
110
97
  override readBinarySync(uri: URI): Uint8Array {
111
- const served = serveVirtualDocument(this.services, uri);
112
- if (served !== undefined) {
113
- return encode(served);
114
- }
115
98
  if (!this.servesFromDisk(uri)) {
116
99
  throw noSuchFile(uri);
117
100
  }
@@ -119,18 +102,10 @@ export class DefaultFileSystemProvider extends NodeFileSystemProvider implements
119
102
  }
120
103
 
121
104
  override stat(uri: URI): Promise<FileSystemNode> {
122
- const served = serveVirtualNode(this.services, uri);
123
- if (served !== undefined) {
124
- return Promise.resolve(served);
125
- }
126
105
  return this.servesFromDisk(uri) ? super.stat(uri) : Promise.reject(noSuchPath(uri));
127
106
  }
128
107
 
129
108
  override statSync(uri: URI): FileSystemNode {
130
- const served = serveVirtualNode(this.services, uri);
131
- if (served !== undefined) {
132
- return served;
133
- }
134
109
  if (!this.servesFromDisk(uri)) {
135
110
  throw noSuchPath(uri);
136
111
  }
@@ -140,16 +115,10 @@ export class DefaultFileSystemProvider extends NodeFileSystemProvider implements
140
115
  // Delegating to `existsSync` would be shorter and would put a blocking stat
141
116
  // on the async path.
142
117
  override async exists(uri: URI): Promise<boolean> {
143
- if (serveVirtualDocument(this.services, uri) !== undefined) {
144
- return true;
145
- }
146
118
  return this.servesFromDisk(uri) ? super.exists(uri) : false;
147
119
  }
148
120
 
149
121
  override existsSync(uri: URI): boolean {
150
- if (serveVirtualDocument(this.services, uri) !== undefined) {
151
- return true;
152
- }
153
122
  return this.servesFromDisk(uri) ? super.existsSync(uri) : false;
154
123
  }
155
124
 
@@ -311,11 +280,6 @@ function unsupportedWrite(uri: URI): Error {
311
280
  return new Error(UNSUPPORTED_WRITE.format({ uri: uri.toString() }));
312
281
  }
313
282
 
314
- /** A virtual document's text as the bytes a binary read returns. */
315
- function encode(text: string): Uint8Array {
316
- return new TextEncoder().encode(text);
317
- }
318
-
319
283
  /**
320
284
  * What a write has to know about the regular file at `path` before replacing
321
285
  * it: the permission bits to carry over, and how many names share its inode.