@hydranium/core 1.0.0-next.200 → 1.0.0-next.202
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 +13 -25
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +0 -56
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/client-session-registry.d.ts +3 -2
- package/lib/documents/client-session-registry.d.ts.map +1 -1
- package/lib/documents/client-session-registry.js +3 -3
- package/lib/documents/client-session-registry.js.map +1 -1
- package/lib/documents/hydranium-text-documents.js +1 -1
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/langium/document-builder/document-builder.d.ts +10 -0
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +13 -0
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts +30 -17
- package/lib/langium/model-service/client-session.d.ts.map +1 -1
- package/lib/langium/model-service/client-session.js +30 -37
- package/lib/langium/model-service/client-session.js.map +1 -1
- package/lib/langium/model-service/index.d.ts +1 -0
- package/lib/langium/model-service/index.d.ts.map +1 -1
- package/lib/langium/model-service/index.js +1 -0
- package/lib/langium/model-service/index.js.map +1 -1
- package/lib/langium/model-service/model-events.d.ts +64 -0
- package/lib/langium/model-service/model-events.d.ts.map +1 -0
- package/lib/langium/model-service/model-events.js +10 -0
- package/lib/langium/model-service/model-events.js.map +1 -0
- package/lib/langium/model-service/model-service.d.ts +74 -38
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +137 -31
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/service-registry.d.ts +14 -0
- package/lib/langium/service-registry.d.ts.map +1 -1
- package/lib/langium/service-registry.js +21 -0
- package/lib/langium/service-registry.js.map +1 -1
- package/lib/langium/transfer/transfer-encoder.d.ts +2 -3
- package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
- package/lib/langium/transfer/transfer-encoder.js +2 -3
- package/lib/langium/transfer/transfer-encoder.js.map +1 -1
- package/lib/langium/workspace/document-uri-policy.d.ts +1 -2
- package/lib/langium/workspace/document-uri-policy.d.ts.map +1 -1
- package/lib/langium/workspace/document-uri-policy.js.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +7 -51
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +0 -57
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-document-builder.d.ts +3 -3
- package/lib/testing/stub-document-builder.d.ts.map +1 -1
- package/lib/testing/stub-document-builder.js +22 -2
- package/lib/testing/stub-document-builder.js.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.js +2 -2
- package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
- package/lib/testing/stub-service-registry.d.ts +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +14 -76
- package/src/documents/client-session-registry.ts +4 -3
- package/src/documents/hydranium-text-documents.ts +1 -1
- package/src/langium/document-builder/document-builder.ts +14 -0
- package/src/langium/model-service/client-session.ts +41 -46
- package/src/langium/model-service/index.ts +1 -0
- package/src/langium/model-service/model-events.ts +74 -0
- package/src/langium/model-service/model-service.ts +204 -42
- package/src/langium/service-registry.ts +22 -0
- package/src/langium/transfer/transfer-encoder.ts +2 -3
- package/src/langium/workspace/document-uri-policy.ts +1 -2
- package/src/testing/stub-ast-document-manager.ts +7 -126
- package/src/testing/stub-document-builder.ts +23 -5
- package/src/testing/stub-hydranium-text-documents.ts +2 -2
- package/src/testing/stub-service-registry.ts +1 -1
|
@@ -27,7 +27,7 @@ import { Disposable } from 'vscode-languageserver';
|
|
|
27
27
|
import { TextDocumentIdentifier, type TextDocumentItem } from 'vscode-languageserver-protocol';
|
|
28
28
|
import { type TextDocument } from 'vscode-languageserver-textdocument';
|
|
29
29
|
import { type ServerSharedServices } from '../langium/module.js';
|
|
30
|
-
import { type HydraniumDocumentBuilder
|
|
30
|
+
import { type HydraniumDocumentBuilder } from '../langium/document-builder/index.js';
|
|
31
31
|
import { UNKNOWN_CLIENT_ID } from './client-ids.js';
|
|
32
32
|
import { type HydraniumTextDocuments } from './hydranium-text-documents.js';
|
|
33
33
|
import { type DocumentUriPolicy } from '../langium/workspace/document-uri-policy.js';
|
|
@@ -38,9 +38,8 @@ export interface UpdateInfo {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Server-internal document envelope at the AST layer
|
|
42
|
-
*
|
|
43
|
-
* for in-process subscribers (`ModelService`, integrity service, etc.).
|
|
41
|
+
* Server-internal document envelope at the AST layer, which `ModelService`
|
|
42
|
+
* reads and events hand to in-process callers.
|
|
44
43
|
*
|
|
45
44
|
* `root` is the live AST the build holds, not an encoded copy like
|
|
46
45
|
* `TransferDocument.model`, so mutating it changes the model every reader sees.
|
|
@@ -83,12 +82,12 @@ export namespace AstDocument {
|
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
/**
|
|
85
|
+
/** The update event a document's build delivers, typed over the generic event wrapper. */
|
|
87
86
|
export type AstDocumentUpdatedEvent<TAst extends AstNode, TDiagnostic extends AstDiagnostic = AstDiagnostic> = TransferUpdatedEvent<
|
|
88
87
|
AstDocument<TAst, TDiagnostic>
|
|
89
88
|
>;
|
|
90
89
|
|
|
91
|
-
/**
|
|
90
|
+
/** The event a save delivers, typed over the generic event wrapper. */
|
|
92
91
|
export type AstDocumentSavedEvent<TAst extends AstNode, TDiagnostic extends AstDiagnostic = AstDiagnostic> = TransferSavedEvent<
|
|
93
92
|
AstDocument<TAst, TDiagnostic>
|
|
94
93
|
>;
|
|
@@ -120,8 +119,8 @@ export interface AstDocumentManagerOptions extends LogNameOptions {
|
|
|
120
119
|
|
|
121
120
|
/**
|
|
122
121
|
* The document slot the model server and the integrity service talk to:
|
|
123
|
-
* open/close/update/save
|
|
124
|
-
* plumbing hidden.
|
|
122
|
+
* open/close/update/save and the attribution of update events, with the LSP
|
|
123
|
+
* plumbing hidden. Events are `ModelService`'s, which builds them from here.
|
|
125
124
|
*
|
|
126
125
|
* Generic over `<TAst extends AstNode, TDiagnostic>` so each consumer projects
|
|
127
126
|
* its own AST root type and diagnostic shape into the emitted
|
|
@@ -152,10 +151,6 @@ export interface AstDocumentManager<TAst extends AstNode, TDiagnostic extends As
|
|
|
152
151
|
*/
|
|
153
152
|
save(uri: string, clientId: string): Promise<TextVersion>;
|
|
154
153
|
|
|
155
|
-
onUpdate(uri: string, listener: (event: AstDocumentUpdatedEvent<TAst, TDiagnostic>) => void): Disposable;
|
|
156
|
-
onSave(uri: string, listener: (event: AstDocumentSavedEvent<TAst, TDiagnostic>) => void | Promise<void>): Disposable;
|
|
157
|
-
onClientClosed(uri: string, clientId: string, listener: () => void): Disposable;
|
|
158
|
-
|
|
159
154
|
getDocument(uri: string): LangiumDocument | undefined;
|
|
160
155
|
|
|
161
156
|
/**
|
|
@@ -174,12 +169,13 @@ export interface AstDocumentManager<TAst extends AstNode, TDiagnostic extends As
|
|
|
174
169
|
|
|
175
170
|
/**
|
|
176
171
|
* The reason, source and cause of an update event for `document` emitted
|
|
177
|
-
* now, from a phase listener of the build that carries it.
|
|
178
|
-
* its update events' attribution from here,
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
172
|
+
* now, from a phase listener of the build that carries it. `ModelService`
|
|
173
|
+
* takes its update events' attribution from here, once per document and
|
|
174
|
+
* phase, so every subscriber names the same client for one build. For a
|
|
175
|
+
* document a client opened, while rebuilds validate, the answer changes
|
|
176
|
+
* once its `Validated` listeners have all run: a later event at that
|
|
177
|
+
* version is `'rebuilt'`. `TransferDocumentUpdateReason` names the cases
|
|
178
|
+
* that fall back to the last update.
|
|
183
179
|
*/
|
|
184
180
|
attributeUpdate(document: LangiumDocument): UpdateAttribution;
|
|
185
181
|
}
|
|
@@ -329,64 +325,6 @@ export class DefaultAstDocumentManager<
|
|
|
329
325
|
return languages.length === 1 ? languages[0].LanguageMetaData.languageId : 'plaintext';
|
|
330
326
|
}
|
|
331
327
|
|
|
332
|
-
/**
|
|
333
|
-
* Subscribe to the save event of the document at `uri`. The callback fires only when the
|
|
334
|
-
* URI of the saved document matches `uri` (compared by canonical form).
|
|
335
|
-
*/
|
|
336
|
-
onSave(uri: string, listener: (event: TransferSavedEvent<AstDocument<TAst, TDiagnostic>>) => void | Promise<void>): Disposable {
|
|
337
|
-
const target = this.uriPolicy.canonicalUri(uri);
|
|
338
|
-
return this.textDocuments.onDidSave(async event => {
|
|
339
|
-
if (this.uriPolicy.canonicalUri(event.document.uri) !== target) {
|
|
340
|
-
return undefined;
|
|
341
|
-
}
|
|
342
|
-
// Look the document up by its canonical URI: the adopter's
|
|
343
|
-
// `LangiumDocuments` keys by the canonical form, while the saved
|
|
344
|
-
// event carries the (possibly symlinked) client-facing URI.
|
|
345
|
-
const documentURI = UriUtils.toUri(target);
|
|
346
|
-
if (documentURI !== undefined && this.langiumDocs.hasDocument(documentURI)) {
|
|
347
|
-
const document = await this.langiumDocs.getOrCreateDocument(documentURI);
|
|
348
|
-
return listener({
|
|
349
|
-
document: this.toAstDocument(document),
|
|
350
|
-
sourceClientId: event.clientId
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
return undefined;
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/** Fires when `clientId` detaches from the document at `uri` (matched by canonical form). */
|
|
358
|
-
onClientClosed(uri: string, clientId: string, listener: () => void): Disposable {
|
|
359
|
-
const target = this.uriPolicy.canonicalUri(uri);
|
|
360
|
-
return this.textDocuments.onDidClose(event => {
|
|
361
|
-
if (event.clientId === clientId && this.uriPolicy.canonicalUri(event.document.uri) === target) {
|
|
362
|
-
listener();
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/** Fires when the document at `uri` reaches `Validated` after each rebuild. */
|
|
368
|
-
onUpdate(uri: string, listener: (event: TransferUpdatedEvent<AstDocument<TAst, TDiagnostic>>) => void): Disposable {
|
|
369
|
-
const target = this.uriPolicy.canonicalUri(uri);
|
|
370
|
-
const emitUpdate = (document: LangiumDocument): void => {
|
|
371
|
-
if (this.uriPolicy.canonicalUri(document.uri) !== target) {
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
// No `'deleted'` reason: `DocumentBuilder.update` drops a deleted
|
|
375
|
-
// document from `LangiumDocuments` before deriving the rebuild set
|
|
376
|
-
// from it, so a deleted URI is never built and never reaches this
|
|
377
|
-
// phase listener. A subscriber needing deletions has to be told on
|
|
378
|
-
// a channel that does not require a built document.
|
|
379
|
-
const event: TransferUpdatedEvent<AstDocument<TAst, TDiagnostic>> = {
|
|
380
|
-
document: this.toAstDocument(document),
|
|
381
|
-
...this.attributeUpdate(document)
|
|
382
|
-
};
|
|
383
|
-
this.tracer.with(uri).trace(`emitUpdate start: source=${event.sourceClientId}, reason=${event.reason}, cause=${event.causedBy}`);
|
|
384
|
-
listener(event);
|
|
385
|
-
this.tracer.with(uri).trace('emitUpdate listener returned');
|
|
386
|
-
};
|
|
387
|
-
return this.documentBuilder.onDocumentPhase(DocumentState.Validated, labelPhaseListener(emitUpdate, 'AstDocumentManager.onUpdate'));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
328
|
/**
|
|
391
329
|
* The client that authored `document`'s current version, or `undefined` when
|
|
392
330
|
* no client wrote it: a document no client has open. A rebuild that keeps
|
|
@@ -36,6 +36,7 @@ export type ClientSessionState = 'live' | 'closing';
|
|
|
36
36
|
/** Delivered by {@link ClientSessionRegistry.onDidCloseSession}. */
|
|
37
37
|
export interface ClientSessionClosedEvent {
|
|
38
38
|
readonly clientId: string;
|
|
39
|
+
readonly cause: SessionEndCause;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -128,12 +129,12 @@ export class ClientSessionRegistry {
|
|
|
128
129
|
return this.opensOf(clientId);
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
/** Remove `clientId` from the session table and announce it
|
|
132
|
-
unregister(clientId: string): void {
|
|
132
|
+
/** Remove `clientId` from the session table and announce it with `cause`. A no-op for an id that is not registered. */
|
|
133
|
+
unregister(clientId: string, cause: SessionEndCause = 'closed'): void {
|
|
133
134
|
if (!this.sessions.delete(clientId)) {
|
|
134
135
|
return;
|
|
135
136
|
}
|
|
136
|
-
this.sessionClosedEmitter.fire(Object.freeze({ clientId }));
|
|
137
|
+
this.sessionClosedEmitter.fire(Object.freeze({ clientId, cause }));
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
/**
|
|
@@ -1685,7 +1685,7 @@ export class HydraniumTextDocuments<T extends TextDocument = TextDocument> exten
|
|
|
1685
1685
|
this.notifyDidCloseTextDocument({ textDocument: { uri } }, clientId, cause);
|
|
1686
1686
|
}
|
|
1687
1687
|
} finally {
|
|
1688
|
-
this.__sessions.unregister(clientId);
|
|
1688
|
+
this.__sessions.unregister(clientId, cause);
|
|
1689
1689
|
this.tracer.info(`Session closed: ${this.formatClientId(clientId)}`);
|
|
1690
1690
|
this.tracer.trace(`Session closed: ${clientId}`);
|
|
1691
1691
|
}
|
|
@@ -30,6 +30,7 @@ import { CancellationToken, Diagnostic, Disposable } from 'vscode-languageserver
|
|
|
30
30
|
import type { HydraniumTextDocuments } from '../../documents/hydranium-text-documents.js';
|
|
31
31
|
import { type VersionSyncService } from '../../documents/version-sync-service.js';
|
|
32
32
|
import { type LogNameOptions } from '../diagnostics/logger.js';
|
|
33
|
+
import { IntegrityService } from '../integrity/integrity-service.js';
|
|
33
34
|
import type { MessageRenderer } from '../../messages/renderer.js';
|
|
34
35
|
import { CST_REHYDRATION_RESET_STATE, isCstShed } from '../residency/cst-residency-service.js';
|
|
35
36
|
import { type ExtendedServiceRegistry } from '../service-registry.js';
|
|
@@ -357,6 +358,19 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
357
358
|
return `current state: '${docState}', last phase: ${lastPhase}, active build: ${this.formatSession(this.activeSession)}`;
|
|
358
359
|
}
|
|
359
360
|
|
|
361
|
+
/**
|
|
362
|
+
* The last phase a rebuild reaches under {@link updateBuildOptions}:
|
|
363
|
+
* `Validated`, or {@link IntegrityService.SettledState} when rebuilds do
|
|
364
|
+
* not validate, since no build then reaches `Validated` and a wait for it
|
|
365
|
+
* would never end. A document a `shouldValidate` override skips stops
|
|
366
|
+
* short of it; waiting on it is this builder's to resolve. An override that
|
|
367
|
+
* returns a phase rebuilds never reach makes every wait for it hang, the
|
|
368
|
+
* session writes that answer at it included.
|
|
369
|
+
*/
|
|
370
|
+
finalBuildState(): DocumentState {
|
|
371
|
+
return this.updateBuildOptions.validation ? DocumentState.Validated : IntegrityService.SettledState;
|
|
372
|
+
}
|
|
373
|
+
|
|
360
374
|
/** Render a session for a status line. `undefined` — no build in progress — reads as `none`. */
|
|
361
375
|
protected formatSession(session: BuildSession | undefined): string {
|
|
362
376
|
if (!session) {
|
|
@@ -19,13 +19,12 @@ import {
|
|
|
19
19
|
type Tracer,
|
|
20
20
|
type TransferElement
|
|
21
21
|
} from '@hydranium/protocol';
|
|
22
|
-
import { type AstNode,
|
|
23
|
-
import { type CancellationToken, type Disposable } from 'vscode-languageserver';
|
|
22
|
+
import { type AstNode, UriUtils } from '@hydranium/langium';
|
|
23
|
+
import { type CancellationToken, type Disposable, Emitter, type Event } from 'vscode-languageserver';
|
|
24
24
|
import { type AstDocument, type SavedAstDocument } from '../../documents/ast-document-manager.js';
|
|
25
25
|
import { DocumentNotOpenError, SessionClosedError } from '../../documents/client-session-errors.js';
|
|
26
26
|
import { type OpenOptions, type SessionEndCause } from '../../documents/client-session-registry.js';
|
|
27
27
|
import { type LogNameOptions } from '../diagnostics/logger.js';
|
|
28
|
-
import { IntegrityService } from '../integrity/integrity-service.js';
|
|
29
28
|
import { type ServerSharedServices } from '../module.js';
|
|
30
29
|
import { type AstDiagnostic } from '../validation/document-validator.js';
|
|
31
30
|
import { type ModelService } from './model-service.js';
|
|
@@ -118,11 +117,11 @@ export interface ClientSession<
|
|
|
118
117
|
save(args: ClientSessionWriteArgs<TTransfer>, cancelToken?: CancellationToken): Promise<SavedAstDocument<TAst, TDiagnostic>>;
|
|
119
118
|
/**
|
|
120
119
|
* Persist the text the store holds for `args.uri` as it is, with no update
|
|
121
|
-
* and no serialisation
|
|
122
|
-
*
|
|
123
|
-
* text.
|
|
120
|
+
* and no serialisation. Resolves with the version of the text written, once
|
|
121
|
+
* it is on disk, without waiting for a build. The saved event names this
|
|
122
|
+
* session, also when another client wrote the text.
|
|
124
123
|
*/
|
|
125
|
-
persist(args: ClientSessionPersistArgs
|
|
124
|
+
persist(args: ClientSessionPersistArgs): Promise<TextVersion>;
|
|
126
125
|
/** Close this session's open of `uri`, at once. A no-op when it does not have `uri` open. */
|
|
127
126
|
close(uri: string): Promise<void>;
|
|
128
127
|
/**
|
|
@@ -142,21 +141,12 @@ export interface ClientSession<
|
|
|
142
141
|
* reason reverts such a document at once.
|
|
143
142
|
*/
|
|
144
143
|
dispose(cause?: SessionEndCause): void;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* `updateBuildOptions`, which every rebuild takes; a document a
|
|
152
|
-
* `shouldValidate` override skips is the builder's to resolve.
|
|
153
|
-
*
|
|
154
|
-
* Waiting for the write's own build is not enough: a later write cancels it,
|
|
155
|
-
* and the build that takes over validates the document after the answer was
|
|
156
|
-
* taken.
|
|
157
|
-
*/
|
|
158
|
-
function answerState(builder: DocumentBuilder): DocumentState {
|
|
159
|
-
return builder.updateBuildOptions.validation ? DocumentState.Validated : IntegrityService.SettledState;
|
|
144
|
+
/**
|
|
145
|
+
* Fires once the session has ended, its documents closed, with the cause it
|
|
146
|
+
* ended with. A takeover through `createSession` ends it as `'lost'` from
|
|
147
|
+
* another caller, so a holder that keeps per-session state releases it here.
|
|
148
|
+
*/
|
|
149
|
+
readonly onDidDispose: Event<SessionEndCause>;
|
|
160
150
|
}
|
|
161
151
|
|
|
162
152
|
/**
|
|
@@ -197,6 +187,7 @@ export class DefaultClientSession<
|
|
|
197
187
|
protected readonly openOptionsByUri = new Map<string, TOpenOptions | undefined>();
|
|
198
188
|
/** Drops an open's options from {@link openOptionsByUri} when the store closes the open. */
|
|
199
189
|
protected readonly closeListener: Disposable;
|
|
190
|
+
protected readonly disposeEmitter = new Emitter<SessionEndCause>();
|
|
200
191
|
/** Logs under `logName`, `ClientSession` when none is given, with this session's id in a bracket of its own, so a line names the session that wrote. */
|
|
201
192
|
protected readonly tracer: Tracer;
|
|
202
193
|
/** The service bound on `model.ModelService`, which serialises and rebuilds for this session. */
|
|
@@ -256,9 +247,9 @@ export class DefaultClientSession<
|
|
|
256
247
|
return this.saveDocument(args, cancelToken);
|
|
257
248
|
}
|
|
258
249
|
|
|
259
|
-
persist(args: ClientSessionPersistArgs
|
|
250
|
+
persist(args: ClientSessionPersistArgs): Promise<TextVersion> {
|
|
260
251
|
this.assertLive();
|
|
261
|
-
return this.persistDocument(args
|
|
252
|
+
return this.persistDocument(args);
|
|
262
253
|
}
|
|
263
254
|
|
|
264
255
|
close(uri: string): Promise<void> {
|
|
@@ -283,6 +274,12 @@ export class DefaultClientSession<
|
|
|
283
274
|
this.disposed = true;
|
|
284
275
|
this.services.workspace.TextDocuments.closeSession(this.clientId, cause);
|
|
285
276
|
this.closeListener.dispose();
|
|
277
|
+
this.disposeEmitter.fire(cause ?? 'closed');
|
|
278
|
+
this.disposeEmitter.dispose();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
get onDidDispose(): Event<SessionEndCause> {
|
|
282
|
+
return this.disposeEmitter.event;
|
|
286
283
|
}
|
|
287
284
|
|
|
288
285
|
protected assertLive(): void {
|
|
@@ -328,7 +325,10 @@ export class DefaultClientSession<
|
|
|
328
325
|
*
|
|
329
326
|
* Resolves with the latest build of the document, which may already carry
|
|
330
327
|
* a newer write; that is logged at debug, and a slow update at warn when
|
|
331
|
-
* `ClientSessionFactoryOptions.slowUpdateWarnMs` is set.
|
|
328
|
+
* `ClientSessionFactoryOptions.slowUpdateWarnMs` is set. It waits for the
|
|
329
|
+
* builder's final phase rather than the write's own build: a later write
|
|
330
|
+
* cancels that build, and the one that takes over validates the document
|
|
331
|
+
* after the answer was taken.
|
|
332
332
|
*/
|
|
333
333
|
protected async updateDocument(
|
|
334
334
|
args: ClientSessionWriteArgs<TTransfer>,
|
|
@@ -353,7 +353,7 @@ export class DefaultClientSession<
|
|
|
353
353
|
// An override that awaits before calling the base can outlast the build
|
|
354
354
|
// this write already has, and then builds it twice; see
|
|
355
355
|
// `ModelService.rebuild`.
|
|
356
|
-
const doc = await run('rebuild', () => service.rebuild(uri,
|
|
356
|
+
const doc = await run('rebuild', () => service.rebuild(uri, this.services.workspace.DocumentBuilder.finalBuildState(), cancelToken));
|
|
357
357
|
const finalVersion = textDocuments.version(uri);
|
|
358
358
|
if (finalVersion > appliedVersion) {
|
|
359
359
|
this.tracer.debug(`Update to v${appliedVersion} ready at v${finalVersion} (changed again before it settled)`);
|
|
@@ -409,50 +409,45 @@ export class DefaultClientSession<
|
|
|
409
409
|
check();
|
|
410
410
|
const applied = uris.map((uri, i) => this.services.workspace.AstDocumentManager.update(uri, texts[i], this.clientId));
|
|
411
411
|
await Promise.all(applied);
|
|
412
|
-
const state =
|
|
412
|
+
const state = this.services.workspace.DocumentBuilder.finalBuildState();
|
|
413
413
|
return Promise.all(uris.map(uri => service.rebuild(uri, state, cancelToken)));
|
|
414
414
|
}
|
|
415
415
|
|
|
416
416
|
/**
|
|
417
|
-
* Write `args.model` through {@link updateDocument},
|
|
418
|
-
* applies to saves too
|
|
417
|
+
* Write `args.model` through {@link updateDocument}, then persist it through
|
|
418
|
+
* {@link persistDocument}, so an override of either applies to saves too.
|
|
419
419
|
*
|
|
420
420
|
* Fails with `DocumentNotOpenError` and writes nothing when this session
|
|
421
|
-
* closes the URI while the text is being built
|
|
422
|
-
*
|
|
423
|
-
* session that closes the URI during the rebuild still has the shared
|
|
424
|
-
* text, other clients' edits included, written in its name. Once taken,
|
|
425
|
-
* the write completes whatever the session does next.
|
|
421
|
+
* closes the URI while the text is being built. Persisted at `'any'`: the
|
|
422
|
+
* update already gated on `args.baseVersion` and moved the text past it.
|
|
426
423
|
*/
|
|
427
424
|
protected async saveDocument(
|
|
428
425
|
args: ClientSessionWriteArgs<TTransfer>,
|
|
429
426
|
cancelToken?: CancellationToken
|
|
430
427
|
): Promise<SavedAstDocument<TAst, TDiagnostic>> {
|
|
431
428
|
const doc = await this.updateDocument(args, cancelToken);
|
|
432
|
-
const
|
|
433
|
-
this.assertOpen(uri);
|
|
434
|
-
const version = await this.services.workspace.AstDocumentManager.save(uri, this.clientId);
|
|
429
|
+
const version = await this.persistDocument({ uri: args.uri, baseVersion: 'any' });
|
|
435
430
|
return { ...doc, persisted: { version } };
|
|
436
431
|
}
|
|
437
432
|
|
|
438
433
|
/**
|
|
439
|
-
* Persist the store's text for `args.uri` under this session's id.
|
|
434
|
+
* Persist the store's text for `args.uri` under this session's id. Every
|
|
435
|
+
* write to disk a session makes goes through here, so an override sees each
|
|
436
|
+
* of them.
|
|
440
437
|
*
|
|
441
438
|
* The open check and the `baseVersion` gate sit in the same synchronous step
|
|
442
439
|
* as the manager taking the text: an await between them lets another
|
|
443
440
|
* client's write land after the gate, and its text is then persisted under
|
|
444
|
-
* this session's id without this session ever having seen it.
|
|
441
|
+
* this session's id without this session ever having seen it. An override
|
|
442
|
+
* that awaits before calling the base lets a diagram save take its
|
|
443
|
+
* documents' texts at different moments, and a diagram that ends during the
|
|
444
|
+
* await fails the documents not yet taken with `DocumentNotOpenError`.
|
|
445
445
|
*/
|
|
446
|
-
protected async persistDocument(
|
|
447
|
-
args: ClientSessionPersistArgs,
|
|
448
|
-
cancelToken?: CancellationToken
|
|
449
|
-
): Promise<SavedAstDocument<TAst, TDiagnostic>> {
|
|
446
|
+
protected async persistDocument(args: ClientSessionPersistArgs): Promise<TextVersion> {
|
|
450
447
|
const uri = this.canonicalKey(args.uri);
|
|
451
448
|
this.assertOpen(uri);
|
|
452
449
|
this.assertBaseVersion(uri, args.baseVersion, this.services.workspace.TextDocuments.version(uri));
|
|
453
|
-
|
|
454
|
-
const doc = await this.modelService.ensureDocumentState(uri, answerState(this.services.workspace.DocumentBuilder), cancelToken);
|
|
455
|
-
return { ...doc, persisted: { version } };
|
|
450
|
+
return this.services.workspace.AstDocumentManager.save(uri, this.clientId);
|
|
456
451
|
}
|
|
457
452
|
|
|
458
453
|
protected async closeDocument(uri: string): Promise<void> {
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
|
|
10
|
+
import { type CanonicalUri } from '@hydranium/protocol';
|
|
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';
|
|
14
|
+
import { type AstDiagnostic } from '../validation/document-validator.js';
|
|
15
|
+
|
|
16
|
+
/** Which documents a `ModelService` subscription hears about. */
|
|
17
|
+
export interface ModelEventFilter {
|
|
18
|
+
/** One document, in any spelling; matched by canonical form. Absent for every document. */
|
|
19
|
+
readonly uri?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A {@link ModelEventFilter} for an event a build phase fires. */
|
|
23
|
+
export interface ModelPhaseFilter extends ModelEventFilter {
|
|
24
|
+
/** Defaults to `Validated`. */
|
|
25
|
+
readonly phase?: DocumentState;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A document reaching {@link phase} in a build.
|
|
30
|
+
*
|
|
31
|
+
* `document` holds for the listener's synchronous run: its root is the one the
|
|
32
|
+
* build carried to the phase, and at `Validated` its diagnostics are that
|
|
33
|
+
* build's, copied. A later build can unlink that root in place or replace it,
|
|
34
|
+
* so a listener that awaits first reads the document again through the phase
|
|
35
|
+
* reads or `snapshot`.
|
|
36
|
+
*
|
|
37
|
+
* Below `Validated` the document carries no diagnostics: any it held would be
|
|
38
|
+
* an earlier build's.
|
|
39
|
+
*/
|
|
40
|
+
export interface ModelUpdatedEvent<TAst extends AstNode, TDiagnostic extends AstDiagnostic = AstDiagnostic> extends AstDocumentUpdatedEvent<
|
|
41
|
+
TAst,
|
|
42
|
+
TDiagnostic
|
|
43
|
+
> {
|
|
44
|
+
readonly phase: DocumentState;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The documents one build carried to {@link phase}. Not fired for a cancelled
|
|
49
|
+
* build, so a document it built shows up in the build that takes over.
|
|
50
|
+
*/
|
|
51
|
+
export interface ModelsBuiltEvent {
|
|
52
|
+
readonly uris: readonly CanonicalUri[];
|
|
53
|
+
readonly phase: DocumentState;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A document's file was removed. No build reaches a deleted document, so no
|
|
58
|
+
* {@link ModelUpdatedEvent} reports it.
|
|
59
|
+
*/
|
|
60
|
+
export interface ModelDeletedEvent {
|
|
61
|
+
readonly uri: CanonicalUri;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A document's text started or stopped differing from its file. */
|
|
65
|
+
export type ModelDirtyChangedEvent = DocumentDirtyChangedEvent;
|
|
66
|
+
|
|
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
|
+
}
|