@hydranium/core 1.0.0-next.185 → 1.0.0-next.188
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 +26 -44
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +20 -50
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/hydranium-text-documents.d.ts +87 -30
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +201 -37
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/index.d.ts +2 -0
- package/lib/documents/index.d.ts.map +1 -1
- package/lib/documents/index.js +2 -0
- package/lib/documents/index.js.map +1 -1
- package/lib/documents/model-ledger.d.ts +47 -0
- package/lib/documents/model-ledger.d.ts.map +1 -0
- package/lib/documents/model-ledger.js +30 -0
- package/lib/documents/model-ledger.js.map +1 -0
- package/lib/documents/version-sync-service.d.ts +134 -0
- package/lib/documents/version-sync-service.d.ts.map +1 -0
- package/lib/documents/version-sync-service.js +171 -0
- package/lib/documents/version-sync-service.js.map +1 -0
- package/lib/langium/bootstrap.d.ts.map +1 -1
- package/lib/langium/bootstrap.js +16 -0
- package/lib/langium/bootstrap.js.map +1 -1
- package/lib/langium/document-builder/document-builder.d.ts +31 -25
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +43 -39
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +6 -15
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts +14 -10
- package/lib/langium/model-service/client-session.d.ts.map +1 -1
- package/lib/langium/model-service/client-session.js +18 -15
- package/lib/langium/model-service/client-session.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +91 -41
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +127 -50
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +6 -0
- package/lib/langium/module.d.ts.map +1 -1
- package/lib/langium/module.js +5 -1
- package/lib/langium/module.js.map +1 -1
- package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
- package/lib/langium/residency/cst-residency-service.js +10 -1
- package/lib/langium/residency/cst-residency-service.js.map +1 -1
- package/lib/langium/shared-services.d.ts +6 -2
- package/lib/langium/shared-services.d.ts.map +1 -1
- package/lib/langium/shared-services.js.map +1 -1
- package/lib/langium/transfer/transfer-encoder.d.ts +19 -10
- package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
- package/lib/langium/transfer/transfer-encoder.js +8 -7
- package/lib/langium/transfer/transfer-encoder.js.map +1 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.d.ts +31 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.js +54 -0
- package/lib/langium/workspace/hydranium-langium-document-factory.js.map +1 -1
- package/lib/langium/workspace/langium-documents.d.ts +7 -0
- package/lib/langium/workspace/langium-documents.d.ts.map +1 -1
- package/lib/langium/workspace/langium-documents.js +10 -0
- package/lib/langium/workspace/langium-documents.js.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +1 -0
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/testing/make-test-services.d.ts +7 -0
- package/lib/testing/make-test-services.d.ts.map +1 -1
- package/lib/testing/make-test-services.js +10 -2
- package/lib/testing/make-test-services.js.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +3 -2
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +7 -1
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.d.ts +2 -2
- package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.js +6 -1
- package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +45 -77
- package/src/documents/hydranium-text-documents.ts +225 -46
- package/src/documents/index.ts +2 -0
- package/src/documents/model-ledger.ts +66 -0
- package/src/documents/version-sync-service.ts +267 -0
- package/src/langium/bootstrap.ts +16 -0
- package/src/langium/document-builder/document-builder.ts +76 -37
- package/src/langium/integrity/integrity-service.ts +6 -15
- package/src/langium/model-service/client-session.ts +29 -21
- package/src/langium/model-service/model-service.ts +182 -64
- package/src/langium/module.ts +9 -1
- package/src/langium/residency/cst-residency-service.ts +10 -1
- package/src/langium/shared-services.ts +6 -2
- package/src/langium/transfer/transfer-encoder.ts +25 -18
- package/src/langium/workspace/hydranium-langium-document-factory.ts +83 -0
- package/src/langium/workspace/langium-documents.ts +11 -0
- package/src/lsp/hydranium-document-update-handler.ts +3 -0
- package/src/testing/make-test-services.ts +15 -2
- package/src/testing/stub-ast-document-manager.ts +10 -2
- package/src/testing/stub-hydranium-text-documents.ts +8 -3
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 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 { DocumentState, type LangiumDocument, type URI } from '@hydranium/langium';
|
|
11
|
+
import { type Tracer, UNRECORDED_VERSION } from '@hydranium/protocol';
|
|
12
|
+
import { Disposable, Emitter, type Event } from 'vscode-languageserver-protocol';
|
|
13
|
+
import { type LogNameOptions } from '../langium/diagnostics/logger.js';
|
|
14
|
+
import { type ServerSharedServices } from '../langium/module.js';
|
|
15
|
+
import { HydraniumWorkspaceLock } from '../langium/workspace/hydranium-workspace-lock.js';
|
|
16
|
+
import { HydraniumTextDocuments } from './hydranium-text-documents.js';
|
|
17
|
+
|
|
18
|
+
/** A build {@link VersionSyncService.requestRecoveryBuild} schedules. */
|
|
19
|
+
export interface RecoveryBuildRequest {
|
|
20
|
+
/** Remove the document instead of building it. */
|
|
21
|
+
readonly deleted?: boolean;
|
|
22
|
+
/** Staged for the build, as `HydraniumDocumentBuilder.scheduleUpdate` takes it. */
|
|
23
|
+
readonly reason?: string;
|
|
24
|
+
/** Asked again inside the lock, where a build that ran meanwhile may have made the request moot. */
|
|
25
|
+
readonly stillNeeded: () => boolean;
|
|
26
|
+
/** Build even while a {@link VersionSyncService.registerDeferredBuilds} check holds the URI back. */
|
|
27
|
+
readonly ignoreDeferred?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Keeps each workspace root's `ModelLedger` record in step with the
|
|
32
|
+
* text store, and owns every build that syncs a root to its text or recovers
|
|
33
|
+
* one a build left behind.
|
|
34
|
+
*/
|
|
35
|
+
export interface VersionSyncService {
|
|
36
|
+
/**
|
|
37
|
+
* Report a root a factory, registry, integrity repair or other producer
|
|
38
|
+
* made. Below `Parsed` it is marked a placeholder. Otherwise `origin.version`,
|
|
39
|
+
* the store's version read before the parse, is recorded; a registered
|
|
40
|
+
* document is then reconciled with the store, re-versioned and recorded at
|
|
41
|
+
* the store's version of its text, built if behind, and announced on
|
|
42
|
+
* {@link onDidRecordModel}. `origin.text` stands for a root whose CST is not
|
|
43
|
+
* the text it describes.
|
|
44
|
+
*/
|
|
45
|
+
modelProduced(document: LangiumDocument, origin?: { version?: number; text?: string }): void;
|
|
46
|
+
/**
|
|
47
|
+
* Build `uri` from a `WorkspaceLock` read, so the build queues behind a
|
|
48
|
+
* running one rather than cancelling it. Requests made while one for `uri`
|
|
49
|
+
* is pending share its next build. Resolves `true` once a build that carried
|
|
50
|
+
* this request completed, or once a check before a build found it no longer
|
|
51
|
+
* needed or deferred. A build a later write cancelled does not count: the
|
|
52
|
+
* request rides the next one. Resolves `false` once builds carrying this
|
|
53
|
+
* request have failed more often than it may retry, so a caller waiting on
|
|
54
|
+
* the build can stop. Never rejects.
|
|
55
|
+
*/
|
|
56
|
+
requestRecoveryBuild(uri: URI, request: RecoveryBuildRequest): Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Build `uri` unless it is {@link isSyncedTo synced to} `textVersion`, asked
|
|
59
|
+
* again inside the lock. `undefined` when no build is needed, else the
|
|
60
|
+
* {@link requestRecoveryBuild} answer.
|
|
61
|
+
*/
|
|
62
|
+
syncTo(uri: URI, textVersion: number): Promise<boolean> | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Whether `uri`'s root was parsed from text at `textVersion` or later; requests
|
|
65
|
+
* nothing. A root with no recorded version or a placeholder counts as synced,
|
|
66
|
+
* since no build records a version for it, and so does a URI with no document.
|
|
67
|
+
*/
|
|
68
|
+
isSyncedTo(uri: URI, textVersion: number): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Register `isDeferred`, which answers whether its caller holds back a build
|
|
71
|
+
* of a URI it will request later, as the update handler does for a debounced
|
|
72
|
+
* editor change. A caller that drops a held change has to leave the URI to
|
|
73
|
+
* another build.
|
|
74
|
+
*/
|
|
75
|
+
registerDeferredBuilds(isDeferred: (uri: URI) => boolean): Disposable;
|
|
76
|
+
/**
|
|
77
|
+
* Fires for every reconciled root. A cancel cannot skip it, unlike a
|
|
78
|
+
* `Parsed` phase listener. It can fire inside a build's write lock, so a
|
|
79
|
+
* listener that requests a build must not await it.
|
|
80
|
+
*/
|
|
81
|
+
readonly onDidRecordModel: Event<LangiumDocument>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A request waiting in {@link PendingRecoveryBuilds}, with the failed builds that carried it. */
|
|
85
|
+
export interface QueuedRecoveryBuild {
|
|
86
|
+
readonly request: RecoveryBuildRequest;
|
|
87
|
+
failures: number;
|
|
88
|
+
readonly answer: (built: boolean) => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The requests waiting on one URI's builds in {@link DefaultVersionSyncService}. */
|
|
92
|
+
export interface PendingRecoveryBuilds {
|
|
93
|
+
readonly queued: QueuedRecoveryBuild[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Collaborators are resolved per call: the registry and its document factory,
|
|
98
|
+
* which construct each other, report roots to this service.
|
|
99
|
+
*/
|
|
100
|
+
export class DefaultVersionSyncService implements VersionSyncService {
|
|
101
|
+
protected readonly tracer: Tracer;
|
|
102
|
+
protected readonly recordedEmitter = new Emitter<LangiumDocument>();
|
|
103
|
+
protected readonly deferredBuildChecks = new Set<(uri: URI) => boolean>();
|
|
104
|
+
protected readonly pendingBuilds = new Map<string, PendingRecoveryBuilds>();
|
|
105
|
+
/** Retries of each request after a failed build before it is answered `false`. */
|
|
106
|
+
protected readonly maxBuildRetries: number = 1;
|
|
107
|
+
|
|
108
|
+
constructor(
|
|
109
|
+
protected readonly services: ServerSharedServices,
|
|
110
|
+
options: LogNameOptions = {}
|
|
111
|
+
) {
|
|
112
|
+
this.tracer = services.Tracer.for(options.logName ?? 'VersionSyncService').trace('instantiated');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get onDidRecordModel(): Event<LangiumDocument> {
|
|
116
|
+
return this.recordedEmitter.event;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
modelProduced(document: LangiumDocument, origin: { version?: number; text?: string } = {}): void {
|
|
120
|
+
const workspace = this.services.workspace;
|
|
121
|
+
const ledger = workspace.ModelLedger;
|
|
122
|
+
const root = document.parseResult.value;
|
|
123
|
+
if (document.state < DocumentState.Parsed) {
|
|
124
|
+
ledger.markPlaceholder(root);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (origin.version !== undefined) {
|
|
128
|
+
ledger.record(root, origin.version, origin.text);
|
|
129
|
+
}
|
|
130
|
+
const store = workspace.TextDocuments;
|
|
131
|
+
// Unregistered, it is a probe or a load whose registration reports it again.
|
|
132
|
+
if (!(store instanceof HydraniumTextDocuments) || workspace.LangiumDocuments.getDocument(document.uri) !== document) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const uri = document.uri.toString();
|
|
136
|
+
// Against the parsed text: the text document may already hold a newer edit.
|
|
137
|
+
const version = store.reconcileExternalContent(uri, origin.text ?? this.parsedText(document));
|
|
138
|
+
if (version !== undefined) {
|
|
139
|
+
if (document.textDocument.version !== version) {
|
|
140
|
+
// An empty-changes update only re-stamps the version.
|
|
141
|
+
store.update(document.textDocument, [], version);
|
|
142
|
+
}
|
|
143
|
+
ledger.record(root, version, origin.text);
|
|
144
|
+
}
|
|
145
|
+
void this.syncTo(document.uri, store.version(uri));
|
|
146
|
+
this.recordedEmitter.fire(document);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
syncTo(uri: URI, textVersion: number): Promise<boolean> | undefined {
|
|
150
|
+
const behind = (): boolean => !this.isSyncedTo(uri, textVersion);
|
|
151
|
+
return behind() ? this.requestRecoveryBuild(uri, { stillNeeded: behind }) : undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
requestRecoveryBuild(uri: URI, request: RecoveryBuildRequest): Promise<boolean> {
|
|
155
|
+
return new Promise<boolean>(answer => {
|
|
156
|
+
const key = uri.toString();
|
|
157
|
+
const queued: QueuedRecoveryBuild = { request, failures: 0, answer };
|
|
158
|
+
const pending = this.pendingBuilds.get(key);
|
|
159
|
+
if (pending) {
|
|
160
|
+
// Not dropped: the pending build may have read the text before this request's change.
|
|
161
|
+
pending.queued.push(queued);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const entry: PendingRecoveryBuilds = { queued: [queued] };
|
|
165
|
+
this.pendingBuilds.set(key, entry);
|
|
166
|
+
void this.runBuilds(key, uri, entry);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
registerDeferredBuilds(isDeferred: (uri: URI) => boolean): Disposable {
|
|
171
|
+
this.deferredBuildChecks.add(isDeferred);
|
|
172
|
+
return Disposable.create(() => this.deferredBuildChecks.delete(isDeferred));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
isSyncedTo(uri: URI, textVersion: number): boolean {
|
|
176
|
+
const root = this.services.workspace.LangiumDocuments.getDocument(uri)?.parseResult.value;
|
|
177
|
+
if (root === undefined) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
const ledger = this.services.workspace.ModelLedger;
|
|
181
|
+
const recorded = ledger.versionOf(root);
|
|
182
|
+
return recorded === UNRECORDED_VERSION || ledger.isPlaceholder(root) || recorded >= textVersion;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The text `document`'s root was parsed from, or for a root built without either record or CST, the document's own. */
|
|
186
|
+
protected parsedText(document: LangiumDocument): string {
|
|
187
|
+
return this.services.workspace.ModelLedger.textOf(document.parseResult.value) ?? document.textDocument.getText();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Run `entry`'s requests, one build per batch, until none is queued, and
|
|
192
|
+
* answer each. A request a failed build carried is queued again
|
|
193
|
+
* {@link maxBuildRetries} times, then answered `false`; one a cancelled
|
|
194
|
+
* build carried is queued again without counting. The entry is dropped
|
|
195
|
+
* in the same tick as the last check of the queue, so no request lands in
|
|
196
|
+
* an entry nothing runs any more.
|
|
197
|
+
*/
|
|
198
|
+
protected async runBuilds(key: string, uri: URI, entry: PendingRecoveryBuilds): Promise<void> {
|
|
199
|
+
const tracer = this.tracer.withUri(key);
|
|
200
|
+
const lock = this.services.workspace.WorkspaceLock;
|
|
201
|
+
// Any other lock resolves a cancelled write as if it had completed, unnoticed.
|
|
202
|
+
const counting = lock instanceof HydraniumWorkspaceLock ? lock : undefined;
|
|
203
|
+
while (entry.queued.length > 0) {
|
|
204
|
+
let carried: QueuedRecoveryBuild[] = [];
|
|
205
|
+
let build: Promise<void> | undefined;
|
|
206
|
+
let cancellations: number | undefined;
|
|
207
|
+
try {
|
|
208
|
+
// The read waits until no write runs or is queued. It must not await
|
|
209
|
+
// the build, a write that queues behind it.
|
|
210
|
+
await lock.read(() => {
|
|
211
|
+
carried = entry.queued.splice(0);
|
|
212
|
+
const needed = carried.filter(queued => this.isStillNeeded(uri, queued.request));
|
|
213
|
+
carried.filter(queued => !needed.includes(queued)).forEach(queued => queued.answer(true));
|
|
214
|
+
carried = needed;
|
|
215
|
+
if (needed.length > 0) {
|
|
216
|
+
build = this.scheduleBatch(
|
|
217
|
+
uri,
|
|
218
|
+
needed.map(queued => queued.request)
|
|
219
|
+
);
|
|
220
|
+
// Read after the build is queued, which cancels the write before it.
|
|
221
|
+
cancellations = counting?.writeCancellations;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
await build;
|
|
225
|
+
// A write cancelled the build, before it ran or while it did.
|
|
226
|
+
if (cancellations !== undefined && counting?.writeCancellations !== cancellations) {
|
|
227
|
+
entry.queued.unshift(...carried);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
carried.forEach(queued => queued.answer(true));
|
|
231
|
+
} catch (err: unknown) {
|
|
232
|
+
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
233
|
+
const retried = carried.filter(queued => queued.failures++ < this.maxBuildRetries);
|
|
234
|
+
const givenUp = carried.filter(queued => !retried.includes(queued));
|
|
235
|
+
if (retried.length > 0) {
|
|
236
|
+
tracer.warn(`Requested build failed; retrying. ${detail}`);
|
|
237
|
+
entry.queued.unshift(...retried);
|
|
238
|
+
}
|
|
239
|
+
if (givenUp.length > 0) {
|
|
240
|
+
tracer.error(`Requested build failed again; giving up. ${detail}`);
|
|
241
|
+
givenUp.forEach(queued => queued.answer(false));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
this.pendingBuilds.delete(key);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Whether `request` still needs its build, and no deferral it heeds holds it back. Runs inside the lock read. */
|
|
249
|
+
protected isStillNeeded(uri: URI, request: RecoveryBuildRequest): boolean {
|
|
250
|
+
return request.stillNeeded() && (request.ignoreDeferred === true || !this.isDeferred(uri));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* One build for every request of `batch`, removing the document when the
|
|
255
|
+
* latest of them asks to, as `HydraniumDocumentBuilder.scheduleUpdate`
|
|
256
|
+
* merges a change and a deletion. Runs inside the lock read.
|
|
257
|
+
*/
|
|
258
|
+
protected scheduleBatch(uri: URI, batch: readonly RecoveryBuildRequest[]): Promise<void> {
|
|
259
|
+
const deleted = batch.at(-1)?.deleted === true;
|
|
260
|
+
const reason = batch.find(request => request.reason !== undefined)?.reason;
|
|
261
|
+
return this.services.workspace.DocumentBuilder.scheduleUpdate(deleted ? [] : [uri], deleted ? [uri] : [], reason);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
protected isDeferred(uri: URI): boolean {
|
|
265
|
+
return [...this.deferredBuildChecks].some(isDeferred => isDeferred(uri));
|
|
266
|
+
}
|
|
267
|
+
}
|
package/src/langium/bootstrap.ts
CHANGED
|
@@ -21,6 +21,8 @@ import { HydraniumScopeComputation } from './scope/hydranium-scope-computation.j
|
|
|
21
21
|
import { HydraniumScopeProvider } from './scope/hydranium-scope-provider.js';
|
|
22
22
|
import { processEnv } from '../util/environment.js';
|
|
23
23
|
import { DefaultTransferEncoder } from './transfer/transfer-encoder.js';
|
|
24
|
+
import { HydraniumLangiumDocumentFactory } from './workspace/hydranium-langium-document-factory.js';
|
|
25
|
+
import { HydraniumLangiumDocuments } from './workspace/langium-documents.js';
|
|
24
26
|
import { HydraniumWorkspaceManager } from './workspace/hydranium-workspace-manager.js';
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -191,6 +193,20 @@ interface StrictSlot {
|
|
|
191
193
|
|
|
192
194
|
const STRICT_SHARED_SLOTS: readonly StrictSlot[] = [
|
|
193
195
|
{ side: 'shared', path: 'workspace.TextDocuments', base: HydraniumTextDocuments, get: s => nested(s, 'workspace', 'TextDocuments') },
|
|
196
|
+
// Every root it builds records no version, so every write gated on one conflicts.
|
|
197
|
+
{
|
|
198
|
+
side: 'shared',
|
|
199
|
+
path: 'workspace.LangiumDocumentFactory',
|
|
200
|
+
base: HydraniumLangiumDocumentFactory,
|
|
201
|
+
get: s => nested(s, 'workspace', 'LangiumDocumentFactory')
|
|
202
|
+
},
|
|
203
|
+
// It reports no registered root, so none is reconciled with the text store or announced.
|
|
204
|
+
{
|
|
205
|
+
side: 'shared',
|
|
206
|
+
path: 'workspace.LangiumDocuments',
|
|
207
|
+
base: HydraniumLangiumDocuments,
|
|
208
|
+
get: s => nested(s, 'workspace', 'LangiumDocuments')
|
|
209
|
+
},
|
|
194
210
|
{
|
|
195
211
|
side: 'shared',
|
|
196
212
|
path: 'workspace.WorkspaceManager',
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type LangiumDocument,
|
|
18
18
|
MultiMap,
|
|
19
19
|
OperationCancelled,
|
|
20
|
-
|
|
20
|
+
URI,
|
|
21
21
|
UriUtils,
|
|
22
22
|
type WorkspaceLock,
|
|
23
23
|
interruptAndCheck,
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
// namespace helper to read the `string | MarkupContent` union without
|
|
28
28
|
// restating it.
|
|
29
29
|
import { CancellationToken, Diagnostic, Disposable } from 'vscode-languageserver-protocol';
|
|
30
|
+
import { type VersionSyncService } from '../../documents/version-sync-service.js';
|
|
30
31
|
import { type LogNameOptions } from '../diagnostics/logger.js';
|
|
31
32
|
import type { MessageRenderer } from '../../messages/renderer.js';
|
|
32
33
|
import { CST_REHYDRATION_RESET_STATE, isCstShed } from '../residency/cst-residency-service.js';
|
|
@@ -126,6 +127,15 @@ export interface DocumentBuilderOptions extends LogNameOptions {
|
|
|
126
127
|
readonly refreshCrossDocumentComputedScopes?: boolean;
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
/** Options for a {@link HydraniumDocumentBuilder.waitUntil} wait on one document. */
|
|
131
|
+
export interface WaitUntilOptions {
|
|
132
|
+
/**
|
|
133
|
+
* Reject once the builder gives up carrying the document to the target
|
|
134
|
+
* state. Without it, such a wait stays pending until its token is cancelled.
|
|
135
|
+
*/
|
|
136
|
+
readonly rejectWhenStuck?: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
/**
|
|
130
140
|
* One locked build {@link HydraniumDocumentBuilder.scheduleUpdate} queued,
|
|
131
141
|
* which later requests merge into while it waits and join while it runs.
|
|
@@ -237,6 +247,8 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
237
247
|
protected readonly buildEndedListeners = new Set<(drained: boolean) => void>();
|
|
238
248
|
/** Set while the lock read {@link checkWaitsOnceDrained} queued has not run. */
|
|
239
249
|
protected drainCheckQueued = false;
|
|
250
|
+
/** Resolved per call: the sync service reaches this builder. */
|
|
251
|
+
protected readonly versionSyncService: () => VersionSyncService;
|
|
240
252
|
|
|
241
253
|
constructor(services: ServerSharedServicesMinimal, options: DocumentBuilderOptions = {}) {
|
|
242
254
|
super(services);
|
|
@@ -245,6 +257,7 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
245
257
|
this.clock = services.Clock;
|
|
246
258
|
this.messageRenderer = services.MessageRenderer;
|
|
247
259
|
this.workspaceLock = services.workspace.WorkspaceLock;
|
|
260
|
+
this.versionSyncService = () => services.workspace.VersionSyncService;
|
|
248
261
|
this.tracer = services.Tracer.for(options.logName ?? 'DocumentBuilder').trace('instantiated');
|
|
249
262
|
this.logLevel = options.logLevel ?? 'debug';
|
|
250
263
|
this.loggedPhases = options.loggedPhases ?? DEFAULT_LOGGED_PHASES;
|
|
@@ -342,6 +355,24 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
342
355
|
// Bug-fixes (always on)
|
|
343
356
|
// ============================================================
|
|
344
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Resolve once the document at `uri` reaches `state`, or without `uri`, once
|
|
360
|
+
* the workspace does. A wait on `uri` re-queues a document no build is
|
|
361
|
+
* carrying, as {@link awaitDocumentState} describes.
|
|
362
|
+
*/
|
|
363
|
+
override waitUntil(state: DocumentState, cancelToken?: CancellationToken): Promise<void>;
|
|
364
|
+
override waitUntil(state: DocumentState, uri?: URI, cancelToken?: CancellationToken, options?: WaitUntilOptions): Promise<URI>;
|
|
365
|
+
override waitUntil(
|
|
366
|
+
state: DocumentState,
|
|
367
|
+
uriOrToken?: URI | CancellationToken,
|
|
368
|
+
cancelToken?: CancellationToken,
|
|
369
|
+
options: WaitUntilOptions = {}
|
|
370
|
+
): Promise<URI | void> {
|
|
371
|
+
return URI.isUri(uriOrToken)
|
|
372
|
+
? this.awaitDocumentState(state, uriOrToken, cancelToken ?? CancellationToken.None, options)
|
|
373
|
+
: super.waitUntil(state, uriOrToken);
|
|
374
|
+
}
|
|
375
|
+
|
|
345
376
|
/**
|
|
346
377
|
* Two edge cases the default Langium implementation rejects on:
|
|
347
378
|
* - Document below target state with no build active (newly-created file):
|
|
@@ -367,7 +398,9 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
367
398
|
* whatever cancelled it may build nothing. Re-queuing stops after
|
|
368
399
|
* {@link MAX_STALLED_REQUEUES} builds that fail to advance the document, so a
|
|
369
400
|
* document the builder will never carry to `state` degrades to a pending wait
|
|
370
|
-
* plus a warning rather than an endless build loop.
|
|
401
|
+
* plus a warning rather than an endless build loop. With `rejectWhenStuck`,
|
|
402
|
+
* the wait rejects instead, both then and once
|
|
403
|
+
* `VersionSyncService.requestRecoveryBuild` gives up a re-queued build.
|
|
371
404
|
*
|
|
372
405
|
* A wait for `Validated` on a document that a validating build skips
|
|
373
406
|
* resolves once that build has indexed its references, without diagnostics:
|
|
@@ -380,7 +413,12 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
380
413
|
* a re-queue: the build waits for that action to end. Await it outside the
|
|
381
414
|
* lock.
|
|
382
415
|
*/
|
|
383
|
-
protected override awaitDocumentState(
|
|
416
|
+
protected override awaitDocumentState(
|
|
417
|
+
state: DocumentState,
|
|
418
|
+
uri: URI,
|
|
419
|
+
cancelToken: CancellationToken,
|
|
420
|
+
{ rejectWhenStuck = false }: WaitUntilOptions = {}
|
|
421
|
+
): Promise<URI> {
|
|
384
422
|
const document = this.langiumDocuments.getDocument(uri);
|
|
385
423
|
if (!document) {
|
|
386
424
|
return super.awaitDocumentState(state, uri, cancelToken);
|
|
@@ -398,15 +436,28 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
398
436
|
// land while a busy workspace keeps cancelling builds.
|
|
399
437
|
let stalledRequeues = 0;
|
|
400
438
|
let lastRequeueState: DocumentState | undefined;
|
|
439
|
+
const giveUp = (reason: string): void => {
|
|
440
|
+
cleanup();
|
|
441
|
+
reject(
|
|
442
|
+
new Error(
|
|
443
|
+
`Gave up building ${this.formatUri(uri)} to '${DocumentState[state]}' ` +
|
|
444
|
+
`(at '${DocumentState[document.state]}'): ${reason}`
|
|
445
|
+
)
|
|
446
|
+
);
|
|
447
|
+
};
|
|
401
448
|
const requeue = (reason: string): void => {
|
|
402
449
|
if (document.state === lastRequeueState) {
|
|
403
450
|
if (++stalledRequeues > MAX_STALLED_REQUEUES) {
|
|
451
|
+
const stalled = `${stalledRequeues} builds did not advance it`;
|
|
452
|
+
if (rejectWhenStuck) {
|
|
453
|
+
giveUp(stalled);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
404
456
|
this.tracer
|
|
405
457
|
.withUri(uri.toString())
|
|
406
458
|
.warn(
|
|
407
459
|
`Giving up re-queuing ${this.formatUri(uri)}: stuck at '${DocumentState[document.state]}', needs ` +
|
|
408
|
-
`'${DocumentState[state]}' after ${
|
|
409
|
-
'The wait now depends on its cancellation token.'
|
|
460
|
+
`'${DocumentState[state]}' after ${stalled}. The wait now depends on its cancellation token.`
|
|
410
461
|
);
|
|
411
462
|
return;
|
|
412
463
|
}
|
|
@@ -414,7 +465,11 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
414
465
|
stalledRequeues = 0;
|
|
415
466
|
lastRequeueState = document.state;
|
|
416
467
|
}
|
|
417
|
-
this.requeueOrphaned(document, state, reason)
|
|
468
|
+
void this.requeueOrphaned(document, state, reason).then(built => {
|
|
469
|
+
if (!built && rejectWhenStuck) {
|
|
470
|
+
giveUp('its recovery build failed');
|
|
471
|
+
}
|
|
472
|
+
});
|
|
418
473
|
};
|
|
419
474
|
const phaseDisposable = this.onDocumentPhase(
|
|
420
475
|
state,
|
|
@@ -465,7 +520,9 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
465
520
|
// advanced this document has already finished, so none of these
|
|
466
521
|
// listeners fires until some other build runs. Re-queue now — the listeners are
|
|
467
522
|
// registered, so the resulting build resolves this wait.
|
|
468
|
-
|
|
523
|
+
// With `rejectWhenStuck`, also a document a failed build left behind:
|
|
524
|
+
// no build ends to re-queue it, so the wait would never give up.
|
|
525
|
+
if (this.isOrphaned(document, state) || (rejectWhenStuck && this.activeSession === undefined)) {
|
|
469
526
|
requeue('quiescent builder');
|
|
470
527
|
}
|
|
471
528
|
});
|
|
@@ -514,39 +571,21 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
|
|
|
514
571
|
}
|
|
515
572
|
|
|
516
573
|
/**
|
|
517
|
-
* Schedule a build for a document no in-flight build will advance
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* The build is a {@link scheduleUpdate}, entered from a {@link WorkspaceLock}
|
|
524
|
-
* read. The read waits until no write runs or is queued, so the build neither
|
|
525
|
-
* runs beside a locked one nor cancels the build a re-queue is fired from.
|
|
526
|
-
* Unlocked, it runs beside a locked build that cannot cancel it: both
|
|
527
|
-
* validate the same version and deliver it twice. Taking the write directly,
|
|
528
|
-
* without the read, cancels the running build and skips the rest of its
|
|
529
|
-
* build-phase listeners. The read must not await the write, which queues
|
|
530
|
-
* behind that read. A build scheduled in the gap between the read batch
|
|
531
|
-
* starting and this action running is still queued, so the re-queue merges
|
|
532
|
-
* into it rather than cancelling it.
|
|
533
|
-
*
|
|
534
|
-
* A write that ran while the read waited may have carried the document to
|
|
535
|
-
* `state` already, and a second build would deliver that version again; the
|
|
536
|
-
* read skips the write then.
|
|
574
|
+
* Schedule a build for a document no in-flight build will advance, through
|
|
575
|
+
* `VersionSyncService.requestRecoveryBuild`: from a {@link WorkspaceLock} read, so
|
|
576
|
+
* the build neither runs beside a locked one nor cancels the build a
|
|
577
|
+
* re-queue is fired from, and skipped if a write that ran meanwhile carried
|
|
578
|
+
* the document to `state`. The waiter resolves off the resulting phase
|
|
579
|
+
* notification; the answer only tells it whether the build was given up.
|
|
537
580
|
*/
|
|
538
|
-
protected requeueOrphaned(document: LangiumDocument, state: DocumentState, reason: string):
|
|
581
|
+
protected requeueOrphaned(document: LangiumDocument, state: DocumentState, reason: string): Promise<boolean> {
|
|
539
582
|
const tracer = this.tracer.withUri(document.uri.toString());
|
|
540
583
|
tracer.info(`Re-queuing orphaned document (at '${DocumentState[document.state]}', needs '${DocumentState[state]}'): ${reason}`);
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (!isOperationCancelled(err)) {
|
|
547
|
-
tracer.error(`Re-queue build failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
548
|
-
}
|
|
549
|
-
});
|
|
584
|
+
return this.versionSyncService().requestRecoveryBuild(document.uri, {
|
|
585
|
+
// Skipped on purpose: a waiting caller is answered sooner, at the cost
|
|
586
|
+
// of a possible duplicate build once the debounce fires.
|
|
587
|
+
ignoreDeferred: true,
|
|
588
|
+
stillNeeded: () => document.state < state
|
|
550
589
|
});
|
|
551
590
|
}
|
|
552
591
|
|
|
@@ -399,28 +399,19 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
399
399
|
// during the re-parse saw the text on disk, which in `'editor'` sync mode
|
|
400
400
|
// is the PRE-repair text. Leave the sequence describing that and the next
|
|
401
401
|
// open hashes the repair, finds a mismatch and steps the version again, so
|
|
402
|
-
// every
|
|
403
|
-
//
|
|
404
|
-
// where there is no sequence to advance; an OPEN document answers
|
|
405
|
-
// `undefined` and keeps the store's own version, which is not this
|
|
402
|
+
// every base version taken from this build is stale before it is used.
|
|
403
|
+
// An open document keeps the store's own version, which is not this
|
|
406
404
|
// method's to move.
|
|
407
405
|
//
|
|
408
406
|
// Deliberately NOT gated on cancellation, unlike the entry to this method:
|
|
409
407
|
// `syncCorrections` has already written or staged the repair by now, so a
|
|
410
408
|
// preempted build that skipped this would leave the sequence describing
|
|
411
409
|
// text that is no longer there.
|
|
412
|
-
//
|
|
413
|
-
// Gated on the document being closed, because the fallback restores a
|
|
414
|
-
// number this method captured off whatever object it started with — and
|
|
415
|
-
// for a separately created document that is its own seeded numbering, not
|
|
416
|
-
// the store's. Applying it to the store's document (which the re-parse has
|
|
417
|
-
// by then made `textDocument`) would roll the shared version backwards.
|
|
418
|
-
// The store's own answer for an open document is `undefined` either way.
|
|
419
410
|
if (!this.textDocuments.isOpen(document.textDocument.uri)) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
411
|
+
// The root describes the repair too; left at the re-parse's number, a
|
|
412
|
+
// syncing read waits for a build nothing requests. The CST is still
|
|
413
|
+
// the unrepaired text, so the repair is reported as the parsed text.
|
|
414
|
+
this.services.shared.workspace.VersionSyncService.modelProduced(document, { text: newText });
|
|
424
415
|
}
|
|
425
416
|
}
|
|
426
417
|
|