@hydranium/core 1.0.0-next.272 → 1.0.0-next.275
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.
- package/lib/documents/ast-document-manager.d.ts +37 -4
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +54 -6
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/dirty-state-tracker.d.ts +15 -0
- package/lib/documents/dirty-state-tracker.d.ts.map +1 -1
- package/lib/documents/dirty-state-tracker.js +14 -1
- package/lib/documents/dirty-state-tracker.js.map +1 -1
- package/lib/documents/hydranium-text-documents.d.ts +14 -0
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +22 -0
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/langium/bootstrap.d.ts +15 -11
- package/lib/langium/bootstrap.d.ts.map +1 -1
- package/lib/langium/bootstrap.js +14 -8
- package/lib/langium/bootstrap.js.map +1 -1
- package/lib/langium/integration-services.d.ts +4 -5
- package/lib/langium/integration-services.d.ts.map +1 -1
- package/lib/langium/integration-services.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts +35 -6
- package/lib/langium/model-service/client-session.d.ts.map +1 -1
- package/lib/langium/model-service/client-session.js +20 -2
- package/lib/langium/model-service/client-session.js.map +1 -1
- package/lib/langium/model-service/model-events.d.ts +5 -0
- package/lib/langium/model-service/model-events.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +7 -0
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +1 -1
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +7 -1
- package/lib/langium/module.d.ts.map +1 -1
- package/lib/langium/module.js +4 -0
- package/lib/langium/module.js.map +1 -1
- package/lib/langium/workspace/file-system-provider.d.ts +13 -5
- package/lib/langium/workspace/file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/file-system-provider.js +11 -5
- package/lib/langium/workspace/file-system-provider.js.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts +4 -2
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.js +9 -3
- package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
- package/lib/langium/workspace/persistent-file-system-provider.d.ts +6 -1
- package/lib/langium/workspace/persistent-file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/persistent-file-system-provider.js +12 -5
- package/lib/langium/workspace/persistent-file-system-provider.js.map +1 -1
- package/lib/langium/workspace/virtual-document.d.ts +2 -1
- package/lib/langium/workspace/virtual-document.d.ts.map +1 -1
- package/lib/langium/workspace/virtual-document.js +5 -2
- package/lib/langium/workspace/virtual-document.js.map +1 -1
- package/lib/langium/workspace/write-lock-scope.d.ts +4 -2
- package/lib/langium/workspace/write-lock-scope.d.ts.map +1 -1
- package/lib/langium/workspace/write-lock-scope.js +6 -4
- package/lib/langium/workspace/write-lock-scope.js.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts +14 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +27 -1
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/messages/index.d.ts +1 -1
- package/lib/messages/index.d.ts.map +1 -1
- package/lib/messages/index.js +1 -1
- package/lib/messages/index.js.map +1 -1
- package/lib/node/node-file-system-provider.d.ts +6 -0
- package/lib/node/node-file-system-provider.d.ts.map +1 -1
- package/lib/node/node-file-system-provider.js +22 -1
- package/lib/node/node-file-system-provider.js.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +10 -0
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.d.ts +1 -1
- package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.js +17 -0
- package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +86 -6
- package/src/documents/dirty-state-tracker.ts +29 -2
- package/src/documents/hydranium-text-documents.ts +24 -0
- package/src/langium/bootstrap.ts +18 -15
- package/src/langium/integration-services.ts +4 -5
- package/src/langium/model-service/client-session.ts +48 -7
- package/src/langium/model-service/model-events.ts +5 -0
- package/src/langium/model-service/model-service.ts +8 -1
- package/src/langium/module.ts +11 -1
- package/src/langium/workspace/file-system-provider.ts +19 -5
- package/src/langium/workspace/in-memory-file-system-provider.ts +13 -3
- package/src/langium/workspace/persistent-file-system-provider.ts +12 -5
- package/src/langium/workspace/virtual-document.ts +6 -2
- package/src/langium/workspace/write-lock-scope.ts +6 -4
- package/src/lsp/hydranium-document-update-handler.ts +30 -1
- package/src/messages/index.ts +1 -1
- package/src/node/node-file-system-provider.ts +22 -1
- package/src/testing/stub-ast-document-manager.ts +22 -1
- package/src/testing/stub-hydranium-text-documents.ts +19 -0
|
@@ -24,6 +24,7 @@ import { type AstNode, DocumentState, type LangiumDocument, type LangiumDocument
|
|
|
24
24
|
import { type LogNameOptions } from '../langium/diagnostics/logger.js';
|
|
25
25
|
import { type AstDiagnostic } from '../langium/validation/document-validator.js';
|
|
26
26
|
import { type WritableFileSystemProvider } from '../langium/workspace/file-system-provider.js';
|
|
27
|
+
import { ReentrantWriteLockError, isInsideWriteLock } from '../langium/workspace/write-lock-scope.js';
|
|
27
28
|
import { Disposable } from 'vscode-languageserver';
|
|
28
29
|
import { TextDocumentIdentifier, type TextDocumentItem } from 'vscode-languageserver-protocol';
|
|
29
30
|
import { type TextDocument } from 'vscode-languageserver-textdocument';
|
|
@@ -151,6 +152,33 @@ export interface AstDocumentManager<TAst extends AstNode, TDiagnostic extends As
|
|
|
151
152
|
* wrote.
|
|
152
153
|
*/
|
|
153
154
|
save(uri: string, clientId: string): Promise<TextVersion>;
|
|
155
|
+
/**
|
|
156
|
+
* Remove the document's file through `FileSystemTaskQueue`, then the
|
|
157
|
+
* document, in a build that closes every open of it but the editor's.
|
|
158
|
+
* Resolves once that build has run, also when it fails after removing the
|
|
159
|
+
* document; rejects, having changed nothing, when the file cannot be
|
|
160
|
+
* removed.
|
|
161
|
+
*
|
|
162
|
+
* From the call until it settles, {@link isDeleting} answers `true`, which
|
|
163
|
+
* refuses every session's write of the document: one landing before the
|
|
164
|
+
* build would be lost with it, or, as a save, write the file back. Marked
|
|
165
|
+
* before the first await, so checks a caller runs in the same step as the
|
|
166
|
+
* call hold up to the removal. A second delete of the document while one is
|
|
167
|
+
* under way is refused, and so is a delete from inside a holder of the write
|
|
168
|
+
* lock, with `ReentrantWriteLockError`, since its builds take that lock.
|
|
169
|
+
*
|
|
170
|
+
* A change requested before the build starts merges over the deletion, and
|
|
171
|
+
* the document lives on with every open but `clientId`'s, which is closed.
|
|
172
|
+
*/
|
|
173
|
+
delete(uri: string, clientId: string): Promise<void>;
|
|
174
|
+
/** Whether a {@link delete} of `uri` is under way. */
|
|
175
|
+
isDeleting(uri: string): boolean;
|
|
176
|
+
/**
|
|
177
|
+
* The client a deletion event for `uri` names: the one whose {@link delete}
|
|
178
|
+
* of it is under way, or the unknown-client id for a file removed outside
|
|
179
|
+
* any, as a file watcher reports one.
|
|
180
|
+
*/
|
|
181
|
+
attributeDeletion(uri: string): string;
|
|
154
182
|
|
|
155
183
|
getDocument(uri: string): LangiumDocument | undefined;
|
|
156
184
|
|
|
@@ -224,10 +252,11 @@ export class DefaultAstDocumentManager<
|
|
|
224
252
|
/**
|
|
225
253
|
* Whose writes the build under way carries, set when it starts: the author
|
|
226
254
|
* of every open document's undelivered version, and the unknown-client id
|
|
227
|
-
* for a change no write explains, such as a deletion
|
|
228
|
-
* has open, or a write that changed nothing.
|
|
229
|
-
* whose build has not validated it yet, so a
|
|
230
|
-
* cancelled stays a cause of the build that takes
|
|
255
|
+
* for a change no write explains, such as a deletion no session's delete
|
|
256
|
+
* made, a document no client has open, or a write that changed nothing.
|
|
257
|
+
* An undelivered version is one whose build has not validated it yet, so a
|
|
258
|
+
* write whose build was cancelled stays a cause of the build that takes
|
|
259
|
+
* over.
|
|
231
260
|
*
|
|
232
261
|
* Errs toward more than one cause, and so toward the unknown client, which
|
|
233
262
|
* a reader treats as foreign: an answer it repeats, never one it misses.
|
|
@@ -240,6 +269,9 @@ export class DefaultAstDocumentManager<
|
|
|
240
269
|
*/
|
|
241
270
|
protected readonly buildCauses = new Set<string>();
|
|
242
271
|
|
|
272
|
+
/** Per canonical URI, the client whose {@link delete} of it is under way. */
|
|
273
|
+
protected readonly deletions = new Map<CanonicalUri, string>();
|
|
274
|
+
|
|
243
275
|
/** Per canonical URI, the newest save queued for it, which {@link coalesceSaves} lets older queued saves defer to. */
|
|
244
276
|
protected readonly newestSaves = new Map<CanonicalUri, Promise<TextVersion>>();
|
|
245
277
|
protected readonly coalesceSaves: boolean;
|
|
@@ -279,9 +311,10 @@ export class DefaultAstDocumentManager<
|
|
|
279
311
|
const textDocument = this.textDocuments.get(uri.toString());
|
|
280
312
|
return textDocument === undefined || this.isDelivered(textDocument.uri, textDocument.version);
|
|
281
313
|
});
|
|
282
|
-
if (unexplained
|
|
314
|
+
if (unexplained) {
|
|
283
315
|
this.buildCauses.add(UNKNOWN_CLIENT_ID);
|
|
284
316
|
}
|
|
317
|
+
deleted.forEach(uri => this.buildCauses.add(this.attributeDeletion(uri.toString())));
|
|
285
318
|
// The only place a deletion is observable: `DocumentBuilder.update`
|
|
286
319
|
// removes a deleted document before it builds anything, so no phase
|
|
287
320
|
// listener ever sees it.
|
|
@@ -440,7 +473,7 @@ export class DefaultAstDocumentManager<
|
|
|
440
473
|
// Text the caller supplies was not read from the file, so no file
|
|
441
474
|
// is assumed: the document is dirty until its first save, whatever
|
|
442
475
|
// the file holds.
|
|
443
|
-
this.textDocuments.
|
|
476
|
+
this.textDocuments.markCreated(args.uri);
|
|
444
477
|
}
|
|
445
478
|
const built = this.getDocument(args.uri);
|
|
446
479
|
if (creates && built) {
|
|
@@ -528,6 +561,53 @@ export class DefaultAstDocumentManager<
|
|
|
528
561
|
return saved;
|
|
529
562
|
}
|
|
530
563
|
|
|
564
|
+
async delete(uri: string, clientId: string): Promise<void> {
|
|
565
|
+
const canonical = this.uriPolicy.canonicalUri(uri);
|
|
566
|
+
const target = UriUtils.toUri(canonical);
|
|
567
|
+
// The builds below wait for a holder of the write lock, which would be
|
|
568
|
+
// waiting for this delete.
|
|
569
|
+
if (isInsideWriteLock()) {
|
|
570
|
+
throw new ReentrantWriteLockError(canonical);
|
|
571
|
+
}
|
|
572
|
+
// The mark holds one client: a second delete would share it, and the
|
|
573
|
+
// first to settle would clear it under the other.
|
|
574
|
+
if (this.deletions.has(canonical)) {
|
|
575
|
+
throw new Error(`Cannot delete ${uri}: a delete of it is under way`);
|
|
576
|
+
}
|
|
577
|
+
this.deletions.set(canonical, clientId);
|
|
578
|
+
try {
|
|
579
|
+
// A document no build has reached yet is not registered, and the
|
|
580
|
+
// deletion build would remove nothing and leave every open of it.
|
|
581
|
+
if (!this.langiumDocs.getDocument(target)) {
|
|
582
|
+
await this.documentBuilder.scheduleUpdate([target], []);
|
|
583
|
+
}
|
|
584
|
+
await this.services.workspace.FileSystemTaskQueue.enqueue(canonical, () => this.fileSystemProvider.deleteFile(target));
|
|
585
|
+
// Langium removes the document before the build's first await, so a
|
|
586
|
+
// failure is the dependents' build, and reaches no one else.
|
|
587
|
+
await this.documentBuilder
|
|
588
|
+
.scheduleUpdate([], [target])
|
|
589
|
+
.catch((error: unknown) =>
|
|
590
|
+
this.tracer.with(canonical).error(`The build after a deletion failed. ${describeErrorWithStack(error)}`)
|
|
591
|
+
);
|
|
592
|
+
// A change requested before that build started merges over the
|
|
593
|
+
// deletion, and the document lives on; the deleting client takes it as
|
|
594
|
+
// gone, so its open ends here.
|
|
595
|
+
if (this.textDocuments.isOpenInClient(canonical, clientId)) {
|
|
596
|
+
await this.close({ uri: canonical, clientId });
|
|
597
|
+
}
|
|
598
|
+
} finally {
|
|
599
|
+
this.deletions.delete(canonical);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
isDeleting(uri: string): boolean {
|
|
604
|
+
return this.deletions.has(this.uriPolicy.canonicalUri(uri));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
attributeDeletion(uri: string): string {
|
|
608
|
+
return this.deletions.get(this.uriPolicy.canonicalUri(uri)) ?? UNKNOWN_CLIENT_ID;
|
|
609
|
+
}
|
|
610
|
+
|
|
531
611
|
/** Write `text` to `uri` unless the file already holds it, then announce the save. Runs inside the file's task queue. */
|
|
532
612
|
protected async writeSave(uri: CanonicalUri, text: string, clientId: string): Promise<void> {
|
|
533
613
|
if (!(await this.matchesDisk(uri, text))) {
|
|
@@ -62,6 +62,18 @@ export interface DirtyStateTracker {
|
|
|
62
62
|
* and for a document not tracked. A first open takes the opener's text.
|
|
63
63
|
*/
|
|
64
64
|
hasDiskBaseline(key: CanonicalUri): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* `key` was created from a session's text, so no file is assumed: no
|
|
67
|
+
* baseline, and {@link isUnsavedCreation} answers `true`. A no-op for a
|
|
68
|
+
* document not tracked.
|
|
69
|
+
*/
|
|
70
|
+
markCreated(key: CanonicalUri, document: TextDocument): void;
|
|
71
|
+
/**
|
|
72
|
+
* Whether `key` is marked created and no baseline with text has been set
|
|
73
|
+
* since, by a save or a reload that found the file. A reload that finds no
|
|
74
|
+
* file leaves the mark. `false` for a document not tracked.
|
|
75
|
+
*/
|
|
76
|
+
isUnsavedCreation(key: CanonicalUri): boolean;
|
|
65
77
|
/**
|
|
66
78
|
* The store released `key`. Answers the clean announcement a document
|
|
67
79
|
* released dirty now owes; `undefined` for one released clean.
|
|
@@ -70,7 +82,7 @@ export interface DirtyStateTracker {
|
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
export class DefaultDirtyStateTracker implements DirtyStateTracker {
|
|
73
|
-
protected readonly states = new Map<CanonicalUri, { baseline: string | undefined; dirty: boolean }>();
|
|
85
|
+
protected readonly states = new Map<CanonicalUri, { baseline: string | undefined; dirty: boolean; created: boolean }>();
|
|
74
86
|
/** Per document released dirty, the token of the announcement it owes. */
|
|
75
87
|
protected readonly owedClean = new Map<CanonicalUri, object>();
|
|
76
88
|
protected readonly dirtyChangedEmitter = new Emitter<DocumentDirtyChangedEvent>();
|
|
@@ -87,7 +99,7 @@ export class DefaultDirtyStateTracker implements DirtyStateTracker {
|
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
track(key: CanonicalUri, document: TextDocument, diskText: string | undefined): void {
|
|
90
|
-
this.states.set(key, { baseline: diskText, dirty: this.owedClean.delete(key) });
|
|
102
|
+
this.states.set(key, { baseline: diskText, dirty: this.owedClean.delete(key), created: false });
|
|
91
103
|
this.refreshDirty(key, document);
|
|
92
104
|
}
|
|
93
105
|
|
|
@@ -111,6 +123,7 @@ export class DefaultDirtyStateTracker implements DirtyStateTracker {
|
|
|
111
123
|
return;
|
|
112
124
|
}
|
|
113
125
|
state.baseline = text;
|
|
126
|
+
state.created &&= text === undefined;
|
|
114
127
|
this.refreshDirty(key, document);
|
|
115
128
|
}
|
|
116
129
|
|
|
@@ -118,6 +131,20 @@ export class DefaultDirtyStateTracker implements DirtyStateTracker {
|
|
|
118
131
|
return this.states.get(key)?.baseline !== undefined;
|
|
119
132
|
}
|
|
120
133
|
|
|
134
|
+
markCreated(key: CanonicalUri, document: TextDocument): void {
|
|
135
|
+
const state = this.states.get(key);
|
|
136
|
+
if (!state) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
state.baseline = undefined;
|
|
140
|
+
state.created = true;
|
|
141
|
+
this.refreshDirty(key, document);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
isUnsavedCreation(key: CanonicalUri): boolean {
|
|
145
|
+
return this.states.get(key)?.created ?? false;
|
|
146
|
+
}
|
|
147
|
+
|
|
121
148
|
release(key: CanonicalUri): CleanAnnouncement | undefined {
|
|
122
149
|
const dirty = this.states.get(key)?.dirty;
|
|
123
150
|
this.states.delete(key);
|
|
@@ -1192,6 +1192,30 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
1192
1192
|
}
|
|
1193
1193
|
}
|
|
1194
1194
|
|
|
1195
|
+
/**
|
|
1196
|
+
* Record that `uri` was created from a session's text rather than read from
|
|
1197
|
+
* its file: it has no disk baseline, so it is dirty until its first save,
|
|
1198
|
+
* and {@link isUnsavedCreation} answers `true` until then. A no-op for a
|
|
1199
|
+
* URI the store does not hold.
|
|
1200
|
+
*/
|
|
1201
|
+
markCreated(uri: DocumentUri): void {
|
|
1202
|
+
const key = this.documentKey(uri);
|
|
1203
|
+
const document = this.__syncedDocuments.get(key);
|
|
1204
|
+
if (document !== undefined) {
|
|
1205
|
+
this.dirtyStateTracker.markCreated(key, document);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Whether `uri` was created from a session's text and the store has not
|
|
1211
|
+
* seen its file since, by a save or a reload that found it. A baseline
|
|
1212
|
+
* cleared because the file went missing does not count. `false` for a URI
|
|
1213
|
+
* the store does not hold.
|
|
1214
|
+
*/
|
|
1215
|
+
isUnsavedCreation(uri: DocumentUri): boolean {
|
|
1216
|
+
return this.dirtyStateTracker.isUnsavedCreation(this.documentKey(uri));
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1195
1219
|
/**
|
|
1196
1220
|
* Read the file behind `uri` through its disk queue and take it as the
|
|
1197
1221
|
* baseline. A file that cannot be read counts as none, the side that
|
package/src/langium/bootstrap.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { type Logger } from '@hydranium/protocol';
|
|
11
11
|
import { type LangiumSharedCoreServices } from '@hydranium/langium';
|
|
12
12
|
import { type LangiumServices, type LangiumSharedServices } from '@hydranium/langium/lsp';
|
|
13
|
-
import type {
|
|
13
|
+
import type { ServerSharedServices } from './module.js';
|
|
14
14
|
import { collectProducibleTypes } from './language-types.js';
|
|
15
15
|
import { assertSingleLangiumCopy } from './single-langium-copy.js';
|
|
16
16
|
import { DefaultAstDocumentManager } from '../documents/ast-document-manager.js';
|
|
@@ -327,17 +327,24 @@ export function warnOnUnexpectedBindings(shared: LangiumSharedServices, language
|
|
|
327
327
|
* BuildPhasePass that sheds the CST of closed documents. The framework
|
|
328
328
|
* default strategy is `always-keep`, so the pass runs but sheds nothing
|
|
329
329
|
* until an adopter configures a shedding strategy.
|
|
330
|
+
* - **`workspace.AstDocumentManager`** — subscribes to the build pipeline
|
|
331
|
+
* and the text store to track opens, deletions and each build's causes.
|
|
332
|
+
* - **`model.ModelService`** — subscribes to the settled document phase to
|
|
333
|
+
* bring the language client up to date. A tree without an LSP `initialize`,
|
|
334
|
+
* such as a headless init, may not resolve it before the first build.
|
|
330
335
|
*
|
|
331
336
|
* Consumers with no additional eager targets can omit the third argument
|
|
332
337
|
* to {@link bootstrapLangium} — the parameter default is this exact set.
|
|
333
338
|
* Adopters needing extra eager targets spread this constant into their own
|
|
334
339
|
* array and append their accessors.
|
|
335
340
|
*/
|
|
336
|
-
export const DEFAULT_EAGER_SERVICES: ReadonlyArray<(services:
|
|
341
|
+
export const DEFAULT_EAGER_SERVICES: ReadonlyArray<(services: ServerSharedServices) => unknown> = [
|
|
337
342
|
services => services.workspace.ProjectManager,
|
|
338
343
|
services => services.workspace.BuildPipelineIntegration,
|
|
339
344
|
services => services.profilers.LangiumProfiler,
|
|
340
|
-
services => services.workspace.CstResidencyService
|
|
345
|
+
services => services.workspace.CstResidencyService,
|
|
346
|
+
services => services.workspace.AstDocumentManager,
|
|
347
|
+
services => services.model.ModelService
|
|
341
348
|
];
|
|
342
349
|
|
|
343
350
|
/**
|
|
@@ -387,7 +394,7 @@ export const DEFAULT_EAGER_SERVICES: ReadonlyArray<(services: ServerSharedServic
|
|
|
387
394
|
* registers every language before validating slots or constructing the
|
|
388
395
|
* eager set.
|
|
389
396
|
*/
|
|
390
|
-
export function bootstrapLangium<TShared extends LangiumSharedServices &
|
|
397
|
+
export function bootstrapLangium<TShared extends LangiumSharedServices & ServerSharedServices, TLang extends LangiumServices>(
|
|
391
398
|
shared: TShared,
|
|
392
399
|
language: TLang,
|
|
393
400
|
eagerlyConstructedServices: ReadonlyArray<(services: TShared) => unknown> = DEFAULT_EAGER_SERVICES
|
|
@@ -420,22 +427,18 @@ export function bootstrapLangium<TShared extends LangiumSharedServices & ServerS
|
|
|
420
427
|
* 4. {@link assertReflectionCoversLanguages} runs **once**, on the one
|
|
421
428
|
* shared `AstReflection`.
|
|
422
429
|
* 5. Eager accessors run **once**, after all registrations. They take
|
|
423
|
-
* the shared tier, and the framework's eager set is shared-tier only
|
|
424
|
-
*
|
|
425
|
-
* `
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
* `documentBuilder` listeners N times and enforce each phase N times.
|
|
430
|
+
* the shared tier, and the framework's eager set is shared-tier only —
|
|
431
|
+
* `BuildPipelineIntegration` in particular routes per document URI to
|
|
432
|
+
* each language's `IntegrityService` / `AstExtensionService`, so it
|
|
433
|
+
* must be constructed knowing all of them. Running the set per language
|
|
434
|
+
* would attach its `documentBuilder` listeners N times and enforce each
|
|
435
|
+
* phase N times.
|
|
430
436
|
*
|
|
431
437
|
* Throws when `languages` is empty: a shared tier with no registered
|
|
432
438
|
* language never validates its slots, which silently defers a
|
|
433
439
|
* composition error to the first build.
|
|
434
440
|
*/
|
|
435
|
-
export function bootstrapLangiumLanguages<
|
|
436
|
-
TShared extends LangiumSharedServices & ServerSharedServicesMinimal,
|
|
437
|
-
TLang extends LangiumServices
|
|
438
|
-
>(
|
|
441
|
+
export function bootstrapLangiumLanguages<TShared extends LangiumSharedServices & ServerSharedServices, TLang extends LangiumServices>(
|
|
439
442
|
shared: TShared,
|
|
440
443
|
languages: ReadonlyArray<TLang>,
|
|
441
444
|
eagerlyConstructedServices: ReadonlyArray<(services: TShared) => unknown> = DEFAULT_EAGER_SERVICES
|
|
@@ -18,8 +18,7 @@ import {
|
|
|
18
18
|
} from '@hydranium/langium/lsp';
|
|
19
19
|
import { DEFAULT_EAGER_SERVICES, bootstrapLangiumLanguages } from './bootstrap.js';
|
|
20
20
|
import { createServerLanguageModule } from './language-module.js';
|
|
21
|
-
import { createServerSharedModule, type ServerModuleContext } from './module.js';
|
|
22
|
-
import type { ServerSharedServicesMinimal } from './shared-services.js';
|
|
21
|
+
import { createServerSharedModule, type ServerModuleContext, type ServerSharedServices } from './module.js';
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
24
|
* Options accepted by {@link createIntegrationServices}. Wraps the
|
|
@@ -40,7 +39,7 @@ import type { ServerSharedServicesMinimal } from './shared-services.js';
|
|
|
40
39
|
*/
|
|
41
40
|
export interface IntegrationServicesOptions<
|
|
42
41
|
TContext extends ServerModuleContext,
|
|
43
|
-
TShared extends LangiumSharedServices &
|
|
42
|
+
TShared extends LangiumSharedServices & ServerSharedServices,
|
|
44
43
|
TLanguage extends LangiumServices
|
|
45
44
|
> {
|
|
46
45
|
/**
|
|
@@ -224,7 +223,7 @@ export interface IntegrationServicesOptions<
|
|
|
224
223
|
* differs only in its grammar-generated module.
|
|
225
224
|
*/
|
|
226
225
|
export interface AdditionalLanguageModules<
|
|
227
|
-
TShared extends LangiumSharedServices &
|
|
226
|
+
TShared extends LangiumSharedServices & ServerSharedServices,
|
|
228
227
|
TLanguage extends LangiumServices
|
|
229
228
|
> {
|
|
230
229
|
/**
|
|
@@ -260,7 +259,7 @@ export interface AdditionalLanguageModules<
|
|
|
260
259
|
*/
|
|
261
260
|
export function createIntegrationServices<
|
|
262
261
|
TContext extends ServerModuleContext,
|
|
263
|
-
TShared extends LangiumSharedServices &
|
|
262
|
+
TShared extends LangiumSharedServices & ServerSharedServices,
|
|
264
263
|
TLanguage extends LangiumServices
|
|
265
264
|
>(
|
|
266
265
|
options: IntegrationServicesOptions<TContext, TShared, TLanguage>
|
|
@@ -46,6 +46,9 @@ export interface ClientSessionWriteArgs<TTransfer> {
|
|
|
46
46
|
/** What a session's `persist` writes: the text the store holds for `uri`. */
|
|
47
47
|
export type ClientSessionPersistArgs = Omit<ClientSessionWriteArgs<never>, 'model'>;
|
|
48
48
|
|
|
49
|
+
/** What a session's `delete` removes: the document at `uri`, at the version it was based on. */
|
|
50
|
+
export type ClientSessionDeleteArgs = Omit<ClientSessionWriteArgs<never>, 'model'>;
|
|
51
|
+
|
|
49
52
|
/** What a session's `updateAll` writes, all or none. */
|
|
50
53
|
export interface ClientSessionUpdateAllArgs<TTransfer> {
|
|
51
54
|
updates: ClientSessionWriteArgs<TTransfer>[];
|
|
@@ -55,11 +58,13 @@ export interface ClientSessionUpdateAllArgs<TTransfer> {
|
|
|
55
58
|
* One participant's handle on the documents it works on, started by
|
|
56
59
|
* `ModelService.createSession`.
|
|
57
60
|
*
|
|
58
|
-
* The session writes only what it has open: `update`, `save
|
|
59
|
-
* fail with `DocumentNotOpenError` unless this session has the URI
|
|
60
|
-
* synchronous step that applies or takes the text,
|
|
61
|
-
*
|
|
62
|
-
*
|
|
61
|
+
* The session writes only what it has open: `update`, `save`, `persist` and
|
|
62
|
+
* `delete` fail with `DocumentNotOpenError` unless this session has the URI
|
|
63
|
+
* open in the synchronous step that applies or takes the text, or marks the
|
|
64
|
+
* document for deletion, so a write either lands while the document is open or
|
|
65
|
+
* fails. They fail the same way while any session's `delete` of the URI is
|
|
66
|
+
* under way. A document stays open until this session closes it, the session
|
|
67
|
+
* ends, or the file is deleted.
|
|
63
68
|
*
|
|
64
69
|
* One open per URI, without reference counting: opening a URI the session
|
|
65
70
|
* already has open changes nothing, and one `close` ends it.
|
|
@@ -123,6 +128,22 @@ export interface ClientSession<
|
|
|
123
128
|
* session, also when another client wrote the text.
|
|
124
129
|
*/
|
|
125
130
|
persist(args: ClientSessionPersistArgs): Promise<TextVersion>;
|
|
131
|
+
/**
|
|
132
|
+
* Delete `args.uri` at once: its file, then the document, as a deletion a
|
|
133
|
+
* file watcher reports removes it. Every session's open of it closes, the
|
|
134
|
+
* editor's stays, and the deletion event names this session. Resolves once
|
|
135
|
+
* the build that removes the document has run. `args.baseVersion` is gated
|
|
136
|
+
* as {@link persist} gates it; a file already gone deletes only the
|
|
137
|
+
* document, and one that cannot be removed fails the call, deleting nothing.
|
|
138
|
+
* The file removed is the one the store keys the document by: under
|
|
139
|
+
* `RealpathDocumentUriPolicy`, a symlink's target rather than the link.
|
|
140
|
+
*
|
|
141
|
+
* A change requested before that build starts, by the editor or a file
|
|
142
|
+
* watcher, keeps the document, and only this session's open of it closes.
|
|
143
|
+
* From inside a build the call fails with `ReentrantWriteLockError`,
|
|
144
|
+
* whatever `ModelServiceOptions.allowReentrantBuilds` says.
|
|
145
|
+
*/
|
|
146
|
+
delete(args: ClientSessionDeleteArgs): Promise<void>;
|
|
126
147
|
/** Close this session's open of `uri`, at once. A no-op when it does not have `uri` open. */
|
|
127
148
|
close(uri: string): Promise<void>;
|
|
128
149
|
/**
|
|
@@ -258,6 +279,11 @@ export class DefaultClientSession<
|
|
|
258
279
|
return this.closeDocument(uri);
|
|
259
280
|
}
|
|
260
281
|
|
|
282
|
+
delete(args: ClientSessionDeleteArgs): Promise<void> {
|
|
283
|
+
this.assertLive();
|
|
284
|
+
return this.deleteDocument(args);
|
|
285
|
+
}
|
|
286
|
+
|
|
261
287
|
withOpen<T>(uri: string, fn: () => MaybePromise<T>): Promise<T> {
|
|
262
288
|
this.assertLive();
|
|
263
289
|
return this.runWithOpen(uri, fn);
|
|
@@ -289,9 +315,10 @@ export class DefaultClientSession<
|
|
|
289
315
|
}
|
|
290
316
|
}
|
|
291
317
|
|
|
292
|
-
/** Throw `DocumentNotOpenError` unless this session has `uri` open. */
|
|
318
|
+
/** Throw `DocumentNotOpenError` unless this session has `uri` open and no delete of it is under way. */
|
|
293
319
|
protected assertOpen(uri: string): void {
|
|
294
|
-
|
|
320
|
+
const workspace = this.services.workspace;
|
|
321
|
+
if (!workspace.TextDocuments.isOpenInClient(uri, this.clientId) || workspace.AstDocumentManager.isDeleting(uri)) {
|
|
295
322
|
throw new DocumentNotOpenError(uri, this.clientId);
|
|
296
323
|
}
|
|
297
324
|
}
|
|
@@ -467,6 +494,20 @@ export class DefaultClientSession<
|
|
|
467
494
|
return this.services.workspace.AstDocumentManager.save(uri, this.clientId);
|
|
468
495
|
}
|
|
469
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Delete `args.uri` under this session's id. The open check and the
|
|
499
|
+
* `baseVersion` gate sit in the same synchronous step as the manager's
|
|
500
|
+
* mark, which refuses every later write of the URI: an await between them
|
|
501
|
+
* lets another client's write land after the gate, and the delete then
|
|
502
|
+
* discards text this session never saw.
|
|
503
|
+
*/
|
|
504
|
+
protected async deleteDocument(args: ClientSessionDeleteArgs): Promise<void> {
|
|
505
|
+
const uri = this.canonicalKey(args.uri);
|
|
506
|
+
this.assertOpen(uri);
|
|
507
|
+
this.assertBaseVersion(uri, args.baseVersion, this.services.workspace.TextDocuments.version(uri));
|
|
508
|
+
return this.services.workspace.AstDocumentManager.delete(uri, this.clientId);
|
|
509
|
+
}
|
|
510
|
+
|
|
470
511
|
protected async closeDocument(uri: string): Promise<void> {
|
|
471
512
|
await this.services.workspace.AstDocumentManager.close({ uri, clientId: this.clientId });
|
|
472
513
|
}
|
|
@@ -60,6 +60,11 @@ export interface ModelsBuiltEvent {
|
|
|
60
60
|
*/
|
|
61
61
|
export interface ModelDeletedEvent {
|
|
62
62
|
readonly uri: CanonicalUri;
|
|
63
|
+
/**
|
|
64
|
+
* The session whose `delete` removed the file; the unknown-client id for a
|
|
65
|
+
* file removed outside any session, as a file watcher reports one.
|
|
66
|
+
*/
|
|
67
|
+
readonly sourceClientId: string;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/** A document was saved: the AST manager's event, as `ModelService` hands it out. */
|
|
@@ -94,6 +94,9 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
94
94
|
* because a document no build will carry is re-queued through the lock, and
|
|
95
95
|
* the re-queue waits for the holder to end.
|
|
96
96
|
*
|
|
97
|
+
* A session's delete is refused that way on either value, since its builds
|
|
98
|
+
* always take the lock.
|
|
99
|
+
*
|
|
97
100
|
* Every other facade build takes the lock on either value. Unlocked, it can
|
|
98
101
|
* overlap another build of the same document: both run a full validation
|
|
99
102
|
* pass that Langium appends, and a caller can be handed a version that an
|
|
@@ -301,6 +304,10 @@ export interface ModelService<
|
|
|
301
304
|
* at `UNRECORDED_VERSION`, whose `root` is `undefined` despite its type.
|
|
302
305
|
*/
|
|
303
306
|
onModelSaved(listener: (event: ModelSavedEvent<TAst, TDiagnostic>) => void, filter?: ModelEventFilter): Disposable;
|
|
307
|
+
/**
|
|
308
|
+
* Fires for every document a build removes, with the session whose
|
|
309
|
+
* `delete` removed it, as `AstDocumentManager.attributeDeletion` names it.
|
|
310
|
+
*/
|
|
304
311
|
onModelDeleted(listener: (event: ModelDeletedEvent) => void, filter?: ModelEventFilter): Disposable;
|
|
305
312
|
/** Fires when a document's text starts or stops differing from its file. */
|
|
306
313
|
onDirtyChanged(listener: (event: ModelDirtyChangedEvent) => void, filter?: ModelEventFilter): Disposable;
|
|
@@ -1218,7 +1225,7 @@ export class DefaultModelService<
|
|
|
1218
1225
|
for (const deletedUri of deleted) {
|
|
1219
1226
|
const uri = this.uriPolicy.canonicalUri(deletedUri.toString());
|
|
1220
1227
|
if (this.matches(target, uri)) {
|
|
1221
|
-
listener(Object.freeze({ uri }));
|
|
1228
|
+
listener(Object.freeze({ uri, sourceClientId: this.services.workspace.AstDocumentManager.attributeDeletion(uri) }));
|
|
1222
1229
|
}
|
|
1223
1230
|
}
|
|
1224
1231
|
});
|
package/src/langium/module.ts
CHANGED
|
@@ -243,7 +243,9 @@ export interface ServerAddedSharedServices<
|
|
|
243
243
|
* there is only one, and `'plaintext'` when there are several, rather
|
|
244
244
|
* than naming a language the document demonstrably is not. Consumers
|
|
245
245
|
* that need to narrow the diagnostic type or override lifecycle
|
|
246
|
-
* behaviour rebind this slot with a subclass.
|
|
246
|
+
* behaviour rebind this slot with a subclass. Subscribes to the build
|
|
247
|
+
* pipeline and the text store in its constructor, so it is eagerly
|
|
248
|
+
* constructed via `DEFAULT_EAGER_SERVICES`.
|
|
247
249
|
*/
|
|
248
250
|
AstDocumentManager: AstDocumentManager<AstNode>;
|
|
249
251
|
/**
|
|
@@ -317,6 +319,10 @@ export interface ServerAddedSharedServices<
|
|
|
317
319
|
*/
|
|
318
320
|
model: {
|
|
319
321
|
TransferEncoder: TransferEncoder<TDiagnostic>;
|
|
322
|
+
/**
|
|
323
|
+
* Subscribes to the settled document phase in its constructor, so it is
|
|
324
|
+
* eagerly constructed via `DEFAULT_EAGER_SERVICES`.
|
|
325
|
+
*/
|
|
320
326
|
ModelService: ModelService<AstNode>;
|
|
321
327
|
/** Builds the handle of every session `ModelService.createSession` starts. */
|
|
322
328
|
ClientSessionFactory: ClientSessionFactory;
|
|
@@ -559,6 +565,8 @@ export function createServerSharedModule(
|
|
|
559
565
|
host: writable ? (fromContext as WritableFileSystemProvider) : new DefaultEmptyFileSystemProvider(services)
|
|
560
566
|
});
|
|
561
567
|
},
|
|
568
|
+
// Eagerly constructed, so its build-pipeline and text-store listeners
|
|
569
|
+
// attach before the first build.
|
|
562
570
|
AstDocumentManager: services => new DefaultAstDocumentManager(services),
|
|
563
571
|
FileSystemTaskQueue: services => new DefaultFileSystemTaskQueue(services),
|
|
564
572
|
// Eagerly constructed, so its build-phase listeners attach before the
|
|
@@ -576,6 +584,8 @@ export function createServerSharedModule(
|
|
|
576
584
|
// Generic walker — adopters with a typed `$type → wire shape` overlay
|
|
577
585
|
// rebind this slot with a subclass.
|
|
578
586
|
TransferEncoder: services => new DefaultTransferEncoder(services),
|
|
587
|
+
// Eagerly constructed, so its settled-phase listener attaches before
|
|
588
|
+
// the first build.
|
|
579
589
|
ModelService: services => new DefaultModelService(services),
|
|
580
590
|
ClientSessionFactory: services => new DefaultClientSessionFactory(services)
|
|
581
591
|
},
|
|
@@ -28,6 +28,12 @@ import { type ServerSharedServicesMinimal } from '../shared-services.js';
|
|
|
28
28
|
*/
|
|
29
29
|
export interface WritableFileSystemProvider extends FileSystemProvider {
|
|
30
30
|
writeFile(uri: URI, content: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Remove the file at `uri`: the name the URI gives, never the file a
|
|
33
|
+
* symlink there points at, leaving a parent directory it empties in place.
|
|
34
|
+
* A no-op when nothing is there; a directory is refused.
|
|
35
|
+
*/
|
|
36
|
+
deleteFile(uri: URI): Promise<void>;
|
|
31
37
|
/**
|
|
32
38
|
* Last-modified time (ms) of the file at `uri`, or `undefined` if it can't
|
|
33
39
|
* be determined (missing file, or a provider with no disk — in-memory /
|
|
@@ -78,11 +84,11 @@ export interface WritableFileSystemProvider extends FileSystemProvider {
|
|
|
78
84
|
|
|
79
85
|
/**
|
|
80
86
|
* Empty-filesystem {@link WritableFileSystemProvider}. For browser / test / CLI
|
|
81
|
-
* hosts that don't have a real filesystem. All writes are no-ops.
|
|
82
|
-
* portable `.` entry it is the
|
|
83
|
-
* `context.fileSystemProvider`
|
|
84
|
-
*
|
|
85
|
-
* `node:fs`).
|
|
87
|
+
* hosts that don't have a real filesystem. All writes and deletes are no-ops.
|
|
88
|
+
* On the portable `.` entry it is the
|
|
89
|
+
* {@link FileSystemProviderRegistry.host} when `context.fileSystemProvider`
|
|
90
|
+
* gives no writable provider; the Node-backed `DefaultFileSystemProvider`
|
|
91
|
+
* lives in `@hydranium/core/node` (it pulls `node:fs`).
|
|
86
92
|
*/
|
|
87
93
|
export class DefaultEmptyFileSystemProvider extends EmptyFileSystemProvider implements WritableFileSystemProvider {
|
|
88
94
|
protected readonly tracer: Tracer;
|
|
@@ -99,6 +105,10 @@ export class DefaultEmptyFileSystemProvider extends EmptyFileSystemProvider impl
|
|
|
99
105
|
// no-op
|
|
100
106
|
}
|
|
101
107
|
|
|
108
|
+
async deleteFile(_uri: URI): Promise<void> {
|
|
109
|
+
// no-op
|
|
110
|
+
}
|
|
111
|
+
|
|
102
112
|
// `async` so the base's refusal becomes a rejection: it throws
|
|
103
113
|
// SYNCHRONOUSLY from a method declared to return a promise, and a
|
|
104
114
|
// synchronous throw escapes the promise chain — `.catch()` never attaches
|
|
@@ -200,6 +210,10 @@ export class DefaultFileSystemProviderRegistry<
|
|
|
200
210
|
return this.providerFor(uri).writeFile(uri, content);
|
|
201
211
|
}
|
|
202
212
|
|
|
213
|
+
async deleteFile(uri: URI): Promise<void> {
|
|
214
|
+
return this.providerFor(uri).deleteFile(uri);
|
|
215
|
+
}
|
|
216
|
+
|
|
203
217
|
async mtimeMs(uri: URI): Promise<number | undefined> {
|
|
204
218
|
return this.providerFor(uri).mtimeMs?.(uri);
|
|
205
219
|
}
|
|
@@ -43,6 +43,12 @@ export const UNSUPPORTED_WRITE = defineMessage(
|
|
|
43
43
|
'Cannot write {uri}: the scheme names no location on disk'
|
|
44
44
|
);
|
|
45
45
|
|
|
46
|
+
/** A delete addressed a URI the provider cannot reach on disk, as {@link UNSUPPORTED_WRITE} a write. */
|
|
47
|
+
export const UNSUPPORTED_DELETE = defineMessage(
|
|
48
|
+
'hydranium/core/unsupported-delete',
|
|
49
|
+
'Cannot delete {uri}: the scheme names no location on disk'
|
|
50
|
+
);
|
|
51
|
+
|
|
46
52
|
/**
|
|
47
53
|
* File content keyed by path, for seeding an {@link InMemoryFileSystemProvider}.
|
|
48
54
|
*
|
|
@@ -136,9 +142,13 @@ export class InMemoryFileSystemProvider implements WritableFileSystemProvider {
|
|
|
136
142
|
this.files.set(normalize(uri), content);
|
|
137
143
|
}
|
|
138
144
|
|
|
139
|
-
/** Remove the file at `uri`. A no-op when nothing is there. */
|
|
140
|
-
deleteFile(uri: URI): void {
|
|
141
|
-
|
|
145
|
+
/** Remove the file at `uri`. A no-op when nothing is there; a directory is refused. */
|
|
146
|
+
async deleteFile(uri: URI): Promise<void> {
|
|
147
|
+
const key = normalize(uri);
|
|
148
|
+
if (this.hasChildren(key)) {
|
|
149
|
+
throw new Error(`Cannot delete ${uri.toString()}: it is a directory`);
|
|
150
|
+
}
|
|
151
|
+
this.files.delete(key);
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
async writeFile(uri: URI, content: string): Promise<void> {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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(
|
|
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
|
}
|