@hydranium/core 1.0.0-next.123 → 1.0.0-next.127
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.map +1 -1
- package/lib/documents/ast-document-manager.js +7 -0
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/hydranium-text-documents.d.ts +62 -0
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +125 -14
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +3 -7
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +8 -7
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +2 -2
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +16 -25
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +12 -5
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/node/socket-launcher.d.ts +1 -1
- package/lib/node/socket-launcher.d.ts.map +1 -1
- package/lib/node/socket-launcher.js +1 -1
- package/lib/node/socket-launcher.js.map +1 -1
- package/lib/node/stdio-launcher.d.ts +1 -1
- package/lib/node/stdio-launcher.d.ts.map +1 -1
- package/lib/node/stdio-launcher.js +1 -1
- package/lib/node/stdio-launcher.js.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.d.ts +5 -3
- package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.js +39 -0
- package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +7 -0
- package/src/documents/hydranium-text-documents.ts +146 -14
- package/src/langium/integrity/integrity-service.ts +8 -7
- package/src/langium/model-service/model-service.ts +16 -25
- package/src/lsp/hydranium-document-update-handler.ts +12 -5
- package/src/node/socket-launcher.ts +1 -2
- package/src/node/stdio-launcher.ts +1 -2
- package/src/testing/stub-hydranium-text-documents.ts +51 -3
|
@@ -461,18 +461,14 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
461
461
|
* authority, and those heads read the correction from the settled build.
|
|
462
462
|
* In silent mode it is also written to disk when the text it was computed
|
|
463
463
|
* from is what disk holds, since the repair is then the only difference;
|
|
464
|
-
* otherwise it waits for the next save. Editor mode always waits, and
|
|
465
|
-
* repair
|
|
466
|
-
* nothing marking it unsaved.
|
|
464
|
+
* otherwise it waits for the next save. Editor mode always waits, and the
|
|
465
|
+
* repair leaves the document dirty until a save.
|
|
467
466
|
* - **Closed, silent mode** → write to disk directly (disk is authoritative).
|
|
468
467
|
* - **Closed, editor mode** → stage so the next open picks it up via applyEdit.
|
|
469
468
|
*
|
|
470
469
|
* A held URI is not closed. Writing it as one persists the holder's unsaved
|
|
471
470
|
* edits with the repair, and staging it leaves text no open reads, because
|
|
472
|
-
* only a first open consumes the stage.
|
|
473
|
-
* is not enough either: a data or GLSP head marks only its own edits unsaved,
|
|
474
|
-
* so a repair of text it never changed would leave disk and store apart with
|
|
475
|
-
* nothing showing it.
|
|
471
|
+
* only a first open consumes the stage.
|
|
476
472
|
*
|
|
477
473
|
* `parsedFrom` is the text the repaired AST was parsed from. Without it a
|
|
478
474
|
* held URI's repair is left to the next save.
|
|
@@ -548,6 +544,10 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
548
544
|
this.tracer.with(document.uri).debug(`Held document unreadable on disk, repair left to the next save: ${String(error)}`);
|
|
549
545
|
return;
|
|
550
546
|
}
|
|
547
|
+
// The file itself, not the store's disk baseline: the baseline moves
|
|
548
|
+
// only when the server hears of a change, so it can still hold the
|
|
549
|
+
// parsed text after another process rewrote the file and before the
|
|
550
|
+
// watcher reports it, and this write would then replace that change.
|
|
551
551
|
if (onDisk !== parsedFrom) {
|
|
552
552
|
return;
|
|
553
553
|
}
|
|
@@ -556,6 +556,7 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
556
556
|
return;
|
|
557
557
|
}
|
|
558
558
|
await this.fileSystemProvider.writeFile(uri, repaired);
|
|
559
|
+
this.textDocuments.updateDiskBaseline(document.uri, repaired);
|
|
559
560
|
});
|
|
560
561
|
}
|
|
561
562
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type MaybeObservableValue,
|
|
14
14
|
type MaybePromise,
|
|
15
15
|
ObservableValue,
|
|
16
|
+
TIMED_OUT,
|
|
16
17
|
type TransferElement,
|
|
17
18
|
type Tracer
|
|
18
19
|
} from '@hydranium/protocol';
|
|
@@ -48,18 +49,9 @@ import { type ClientSession } from './client-session.js';
|
|
|
48
49
|
*/
|
|
49
50
|
export const MODEL_UPDATE_EDIT = defineMessage('hydranium/core/model-update-edit', 'Update Model');
|
|
50
51
|
|
|
51
|
-
/** Max time {@link
|
|
52
|
+
/** Max time {@link DefaultModelService.settleSave} waits for the build to settle and the sync chain to drain. */
|
|
52
53
|
const SAVE_SETTLE_TIMEOUT_MS = 10_000;
|
|
53
54
|
|
|
54
|
-
/**
|
|
55
|
-
* Marks the {@link SAVE_SETTLE_TIMEOUT_MS} branch of
|
|
56
|
-
* {@link ModelService.settleSave}'s race so it stays distinguishable from a
|
|
57
|
-
* genuine rejection (a cancelled token, an `applyEdit` reverse-RPC error, a
|
|
58
|
-
* build throw). With a plain `Error` the only log line an adopter has blames
|
|
59
|
-
* the timeout for every one of them, which points debugging at the wrong layer.
|
|
60
|
-
*/
|
|
61
|
-
class SaveSettleTimeoutError extends Error {}
|
|
62
|
-
|
|
63
55
|
/**
|
|
64
56
|
* Constructor options for {@link ModelService}. All fields are optional,
|
|
65
57
|
* and the defaults are the behaviour described on each one.
|
|
@@ -672,8 +664,8 @@ export class DefaultModelService<
|
|
|
672
664
|
* so every language client reflects the latest content before a save returns.
|
|
673
665
|
* No-op tail for headless adopters — `syncChains` is empty without an LSP
|
|
674
666
|
* client. Bounded by {@link SAVE_SETTLE_TIMEOUT_MS} so a hung build, or an
|
|
675
|
-
* applyEdit reverse-RPC deadlock, can't freeze the caller; on timeout
|
|
676
|
-
* a warning and returns rather than throwing.
|
|
667
|
+
* applyEdit reverse-RPC deadlock, can't freeze the caller; on timeout or a
|
|
668
|
+
* failed wait it logs a warning and returns rather than throwing.
|
|
677
669
|
*
|
|
678
670
|
* Adopters with a save flow that must converge editor + disk before returning
|
|
679
671
|
* (e.g. a dual form/code editor that would otherwise show a content-conflict
|
|
@@ -682,22 +674,21 @@ export class DefaultModelService<
|
|
|
682
674
|
protected async settleSave(uri: string, cancelToken?: CancellationToken): Promise<void> {
|
|
683
675
|
const canonical = this.uriPolicy.canonicalUri(uri);
|
|
684
676
|
const key = UriUtils.toUri(canonical).toString();
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
)
|
|
677
|
+
// One race over both waits, so the bound is one deadline for the pair;
|
|
678
|
+
// a race per wait would let the save take twice the bound.
|
|
679
|
+
const settled = (async (): Promise<void> => {
|
|
680
|
+
await this.waitForDocumentStateCanonical(canonical, IntegrityService.SettledState, cancelToken);
|
|
681
|
+
await this.syncChains.get(key);
|
|
682
|
+
})();
|
|
688
683
|
try {
|
|
689
|
-
await
|
|
690
|
-
const pending = this.syncChains.get(key);
|
|
691
|
-
if (pending) {
|
|
692
|
-
await Promise.race([pending, timeout]);
|
|
693
|
-
}
|
|
694
|
-
} catch (err: unknown) {
|
|
695
|
-
if (err instanceof SaveSettleTimeoutError) {
|
|
684
|
+
if ((await this.services.Clock.raceTimer(settled, SAVE_SETTLE_TIMEOUT_MS)) === TIMED_OUT) {
|
|
696
685
|
this.tracer.withUri(key).warn(`Save settle exceeded ${SAVE_SETTLE_TIMEOUT_MS}ms — returning anyway`);
|
|
697
|
-
} else {
|
|
698
|
-
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
699
|
-
this.tracer.withUri(key).warn(`Save settle failed before the timeout — returning anyway. ${detail}`);
|
|
700
686
|
}
|
|
687
|
+
} catch (err: unknown) {
|
|
688
|
+
// A failed wait, such as a cancelled token or a document the builder
|
|
689
|
+
// does not hold, gets its own line so it is not blamed on the bound.
|
|
690
|
+
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
691
|
+
this.tracer.withUri(key).warn(`Save settle failed before the timeout — returning anyway. ${detail}`);
|
|
701
692
|
}
|
|
702
693
|
}
|
|
703
694
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { Debouncer, Deferred, type Logger, ObservableValue, type MaybeObservableValue } from '@hydranium/protocol';
|
|
10
|
+
import { Debouncer, Deferred, type Logger, ObservableValue, type MaybeObservableValue, TIMED_OUT } from '@hydranium/protocol';
|
|
11
11
|
import { URI, UriUtils } from '@hydranium/langium';
|
|
12
12
|
import { DefaultDocumentUpdateHandler } from '@hydranium/langium/lsp';
|
|
13
13
|
import { HYDRANIUM_BUILD_REASONS, type HydraniumDocumentBuilder } from '../langium/document-builder/document-builder.js';
|
|
@@ -212,6 +212,15 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
212
212
|
if (filtered.changes.length === 0) {
|
|
213
213
|
return;
|
|
214
214
|
}
|
|
215
|
+
// Only an open document keeps the file's text as its disk
|
|
216
|
+
// baseline; the store ignores the rest. The self-save filter has
|
|
217
|
+
// dropped the server's own writes, which set the baseline when
|
|
218
|
+
// they wrote.
|
|
219
|
+
for (const change of filtered.changes) {
|
|
220
|
+
this.textDocuments
|
|
221
|
+
.reloadDiskBaseline(change.uri)
|
|
222
|
+
.catch((err: unknown) => this.logger.error(`Disk baseline not reloaded for ${change.uri}. ${String(err)}`));
|
|
223
|
+
}
|
|
215
224
|
// External file-system changes are authoritative — overwrite any
|
|
216
225
|
// pending reason (typically empty here; could be `didChangeContent`
|
|
217
226
|
// if a watcher event raced an in-flight Monaco edit, in which case
|
|
@@ -273,14 +282,12 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
273
282
|
void this.services.workspace.FileSystemTaskQueue.enqueue(uri, async () => {
|
|
274
283
|
cap.dispose();
|
|
275
284
|
resolve([]);
|
|
276
|
-
|
|
285
|
+
if ((await clock.raceTimer(hold.promise, this.willSaveGateMs)) === TIMED_OUT) {
|
|
277
286
|
this.logger.debug(
|
|
278
287
|
`No didSave for ${uri} within ${this.willSaveGateMs} ms; server writes of it no longer wait for the editor's save.`
|
|
279
288
|
);
|
|
280
289
|
release();
|
|
281
|
-
}
|
|
282
|
-
await hold.promise;
|
|
283
|
-
bound.dispose();
|
|
290
|
+
}
|
|
284
291
|
});
|
|
285
292
|
} catch (err) {
|
|
286
293
|
cap.dispose();
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { Deferred } from '@hydranium/langium';
|
|
11
10
|
import * as net from 'node:net';
|
|
12
|
-
import type
|
|
11
|
+
import { Deferred, type Disposable, type Logger } from '@hydranium/protocol';
|
|
13
12
|
import { createMessageConnection, SocketMessageReader, SocketMessageWriter } from 'vscode-jsonrpc/node';
|
|
14
13
|
import type { MessageConnection } from 'vscode-jsonrpc';
|
|
15
14
|
import type { IntegratedServer } from '../launcher/integrated-server.js';
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { Deferred } from '@hydranium/
|
|
11
|
-
import type { Disposable, Logger } from '@hydranium/protocol';
|
|
10
|
+
import { Deferred, type Disposable, type Logger } from '@hydranium/protocol';
|
|
12
11
|
import type { MessageConnection } from 'vscode-jsonrpc';
|
|
13
12
|
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node';
|
|
14
13
|
import type { IntegratedServer } from '../launcher/integrated-server.js';
|
|
@@ -13,7 +13,12 @@ import { Disposable, Emitter } from 'vscode-languageserver';
|
|
|
13
13
|
import type { TextDocument } from 'vscode-languageserver-textdocument';
|
|
14
14
|
import { LANGUAGE_CLIENT_ID } from '../documents/client-ids.js';
|
|
15
15
|
import { ClientSessionRegistry } from '../documents/client-session-registry.js';
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
ClientTextDocumentChangeEvent,
|
|
18
|
+
DocumentDirtyChangedEvent,
|
|
19
|
+
HydraniumTextDocuments,
|
|
20
|
+
LastOpenClosedEvent
|
|
21
|
+
} from '../documents/hydranium-text-documents.js';
|
|
17
22
|
|
|
18
23
|
/** Snapshot of an open document tracked by the stub. */
|
|
19
24
|
export interface StubTextDocumentEntry {
|
|
@@ -31,7 +36,9 @@ export interface StubTextDocumentEntry {
|
|
|
31
36
|
* `isOpenInAnyClient` / `isOpenInClient` / `isRevertPending` /
|
|
32
37
|
* `openDocuments`), the push channel
|
|
33
38
|
* to the language client (`applyEditToLanguageClient` / `stagePendingContent`),
|
|
34
|
-
* the save / close notifications,
|
|
39
|
+
* the save / close notifications, the dirty state (`isDirty` /
|
|
40
|
+
* `onDidChangeDirty` / `updateDiskBaseline`, against a baseline {@link seedOpen}
|
|
41
|
+
* sets and a save that carries its text moves), and the client-session table
|
|
35
42
|
* (`registerSession` / `closeSession` / `onDidCloseSession`), which is a real
|
|
36
43
|
* `ClientSessionRegistry` — plus test-only helpers:
|
|
37
44
|
*
|
|
@@ -80,6 +87,9 @@ export interface StubHydraniumTextDocuments extends Pick<
|
|
|
80
87
|
| 'closeSession'
|
|
81
88
|
| 'onDidCloseSession'
|
|
82
89
|
| 'onDidCloseLastOpen'
|
|
90
|
+
| 'isDirty'
|
|
91
|
+
| 'onDidChangeDirty'
|
|
92
|
+
| 'updateDiskBaseline'
|
|
83
93
|
> {
|
|
84
94
|
/**
|
|
85
95
|
* Stub-tailored read accessor. Returns just the surface the framework's
|
|
@@ -98,7 +108,7 @@ export interface StubHydraniumTextDocuments extends Pick<
|
|
|
98
108
|
onDidSave(listener: (event: { document: { uri: string }; clientId: string }) => void): Disposable;
|
|
99
109
|
/** Real-shaped close subscription — listener gets {@link ClientTextDocumentChangeEvent}. */
|
|
100
110
|
onDidClose(listener: (event: ClientTextDocumentChangeEvent<TextDocument>) => void): Disposable;
|
|
101
|
-
/** Pre-populate an open document without firing change events. */
|
|
111
|
+
/** Pre-populate an open document without firing change events; `text` is also its disk baseline. */
|
|
102
112
|
seedOpen(uri: string, text: string, clientId: string): void;
|
|
103
113
|
/** Mark `uri` as open in the LSP textual language client (drives {@link isOpenInLanguageClient}). */
|
|
104
114
|
seedOpenInLanguageClient(uri: string): void;
|
|
@@ -152,6 +162,23 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
|
|
|
152
162
|
const sessions = new ClientSessionRegistry();
|
|
153
163
|
const key = (uri: string): CanonicalUri => uri as CanonicalUri;
|
|
154
164
|
const lastOpenClosed = new Emitter<LastOpenClosedEvent>();
|
|
165
|
+
const baselines = new Map<string, string | undefined>();
|
|
166
|
+
const dirty = new Set<string>();
|
|
167
|
+
const dirtyChanged = new Emitter<DocumentDirtyChangedEvent>();
|
|
168
|
+
// As the real store: dirty while held with text other than the baseline,
|
|
169
|
+
// announced only when the answer changes.
|
|
170
|
+
const refreshDirty = (uri: string): void => {
|
|
171
|
+
const held = docs.get(uri);
|
|
172
|
+
const now = held !== undefined && held.text !== baselines.get(uri);
|
|
173
|
+
if (now !== dirty.has(uri)) {
|
|
174
|
+
if (now) {
|
|
175
|
+
dirty.add(uri);
|
|
176
|
+
} else {
|
|
177
|
+
dirty.delete(uri);
|
|
178
|
+
}
|
|
179
|
+
dirtyChanged.fire({ uri: key(uri), dirty: now });
|
|
180
|
+
}
|
|
181
|
+
};
|
|
155
182
|
|
|
156
183
|
const stub: StubHydraniumTextDocuments = {
|
|
157
184
|
get changes() {
|
|
@@ -186,6 +213,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
|
|
|
186
213
|
}
|
|
187
214
|
docs.set(event.textDocument.uri, next);
|
|
188
215
|
changes.push(next);
|
|
216
|
+
refreshDirty(event.textDocument.uri);
|
|
189
217
|
},
|
|
190
218
|
applyContentChange(uri, text, clientId) {
|
|
191
219
|
const existing = docs.get(uri);
|
|
@@ -199,9 +227,13 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
|
|
|
199
227
|
const next: StubTextDocumentEntry = changed ? { uri, version: existing.version + 1, text, clientId } : existing;
|
|
200
228
|
docs.set(uri, next);
|
|
201
229
|
changes.push({ uri, version: next.version, text, clientId });
|
|
230
|
+
refreshDirty(uri);
|
|
202
231
|
return next.version;
|
|
203
232
|
},
|
|
204
233
|
notifyDidSaveTextDocument(event, clientId) {
|
|
234
|
+
if (event.text !== undefined) {
|
|
235
|
+
stub.updateDiskBaseline(event.textDocument.uri, event.text);
|
|
236
|
+
}
|
|
205
237
|
saves.push({ uri: event.textDocument.uri, clientId });
|
|
206
238
|
for (const listener of saveListeners.slice()) {
|
|
207
239
|
listener({ document: { uri: event.textDocument.uri }, clientId });
|
|
@@ -264,6 +296,18 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
|
|
|
264
296
|
get onDidCloseLastOpen() {
|
|
265
297
|
return lastOpenClosed.event;
|
|
266
298
|
},
|
|
299
|
+
isDirty(uri) {
|
|
300
|
+
return dirty.has(uri);
|
|
301
|
+
},
|
|
302
|
+
get onDidChangeDirty() {
|
|
303
|
+
return dirtyChanged.event;
|
|
304
|
+
},
|
|
305
|
+
updateDiskBaseline(uri, text) {
|
|
306
|
+
if (docs.has(uri)) {
|
|
307
|
+
baselines.set(uri, text);
|
|
308
|
+
refreshDirty(uri);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
267
311
|
openDocuments() {
|
|
268
312
|
// From the registry that also answers `isOpenInClient`, which both
|
|
269
313
|
// seeding channels feed, so the two never disagree about an open.
|
|
@@ -282,6 +326,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
|
|
|
282
326
|
seedOpen(uri, text, clientId) {
|
|
283
327
|
sessions.addOpen(key(uri), clientId);
|
|
284
328
|
docs.set(uri, { uri, version: 1, text, clientId });
|
|
329
|
+
baselines.set(uri, text);
|
|
285
330
|
},
|
|
286
331
|
seedOpenInLanguageClient(uri) {
|
|
287
332
|
sessions.addOpen(key(uri), LANGUAGE_CLIENT_ID);
|
|
@@ -294,6 +339,7 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
|
|
|
294
339
|
const holder = docs.get(uri);
|
|
295
340
|
if (holder && holder.clientId === clientId) {
|
|
296
341
|
docs.delete(uri);
|
|
342
|
+
refreshDirty(uri);
|
|
297
343
|
}
|
|
298
344
|
const removed = sessions.removeOpen(key(uri), clientId);
|
|
299
345
|
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
@@ -314,6 +360,8 @@ export function makeStubHydraniumTextDocuments(): StubHydraniumTextDocuments {
|
|
|
314
360
|
languageClientOpen.clear();
|
|
315
361
|
appliedEdits.length = 0;
|
|
316
362
|
staged.length = 0;
|
|
363
|
+
baselines.clear();
|
|
364
|
+
dirty.clear();
|
|
317
365
|
saveListeners.length = 0;
|
|
318
366
|
closeListeners.length = 0;
|
|
319
367
|
applyEditHandler = () => ({ applied: true });
|