@hydranium/core 1.0.0-next.117 → 1.0.0-next.123
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 +8 -44
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +22 -41
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/client-session-errors.d.ts +4 -33
- package/lib/documents/client-session-errors.d.ts.map +1 -1
- package/lib/documents/client-session-errors.js +4 -46
- package/lib/documents/client-session-errors.js.map +1 -1
- package/lib/documents/client-session-registry.d.ts +40 -20
- package/lib/documents/client-session-registry.d.ts.map +1 -1
- package/lib/documents/client-session-registry.js +33 -20
- package/lib/documents/client-session-registry.js.map +1 -1
- package/lib/documents/file-system-task-queue.d.ts +49 -0
- package/lib/documents/file-system-task-queue.d.ts.map +1 -0
- package/lib/documents/file-system-task-queue.js +34 -0
- package/lib/documents/file-system-task-queue.js.map +1 -0
- package/lib/documents/hydranium-text-documents.d.ts +138 -13
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +283 -40
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/index.d.ts +1 -0
- package/lib/documents/index.d.ts.map +1 -1
- package/lib/documents/index.js +1 -0
- package/lib/documents/index.js.map +1 -1
- package/lib/langium/document-builder/document-builder.d.ts +15 -2
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +15 -2
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +3 -2
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +9 -6
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/language-module.d.ts +2 -2
- package/lib/langium/language-module.js +2 -2
- package/lib/langium/language-module.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts +203 -24
- package/lib/langium/model-service/client-session.d.ts.map +1 -1
- package/lib/langium/model-service/client-session.js +233 -29
- package/lib/langium/model-service/client-session.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +62 -217
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +54 -357
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +11 -1
- 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/update-rewrite/update-rewrite.d.ts +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.js +10 -2
- package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
- package/lib/langium/workspace/write-lock-scope.d.ts +1 -1
- package/lib/langium/workspace/write-lock-scope.js +1 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts +69 -67
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +111 -89
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/lsp/shared-module.d.ts +4 -8
- package/lib/lsp/shared-module.d.ts.map +1 -1
- package/lib/lsp/shared-module.js.map +1 -1
- package/lib/lsp/start-language-server.d.ts +3 -4
- package/lib/lsp/start-language-server.d.ts.map +1 -1
- package/lib/lsp/start-language-server.js +3 -4
- package/lib/lsp/start-language-server.js.map +1 -1
- package/lib/testing/make-test-services.d.ts +16 -4
- package/lib/testing/make-test-services.d.ts.map +1 -1
- package/lib/testing/make-test-services.js +6 -2
- package/lib/testing/make-test-services.js.map +1 -1
- package/lib/testing/node/lsp-server-connection.d.ts +1 -1
- package/lib/testing/node/scratch-workspace.js +1 -1
- package/lib/testing/run-update-pipeline.d.ts +4 -4
- package/lib/testing/run-update-pipeline.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +5 -5
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +6 -8
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.d.ts +10 -5
- package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.js +16 -9
- package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
- package/lib/testing/stub-model-service.d.ts +2 -2
- package/lib/testing/stub-model-service.js +2 -2
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +27 -78
- package/src/documents/client-session-errors.ts +11 -43
- package/src/documents/client-session-registry.ts +54 -28
- package/src/documents/file-system-task-queue.ts +68 -0
- package/src/documents/hydranium-text-documents.ts +348 -44
- package/src/documents/index.ts +1 -0
- package/src/langium/document-builder/document-builder.ts +16 -2
- package/src/langium/integrity/integrity-service.ts +9 -6
- package/src/langium/language-module.ts +4 -4
- package/src/langium/model-service/client-session.ts +363 -39
- package/src/langium/model-service/model-service.ts +91 -420
- package/src/langium/module.ts +14 -2
- package/src/langium/update-rewrite/update-rewrite.ts +1 -1
- package/src/langium/workspace/in-memory-file-system-provider.ts +11 -2
- package/src/langium/workspace/write-lock-scope.ts +1 -1
- package/src/lsp/hydranium-document-update-handler.ts +138 -92
- package/src/lsp/shared-module.ts +4 -8
- package/src/lsp/start-language-server.ts +3 -4
- package/src/testing/make-test-services.ts +25 -6
- package/src/testing/node/lsp-server-connection.ts +1 -1
- package/src/testing/node/scratch-workspace.ts +1 -1
- package/src/testing/run-update-pipeline.ts +4 -4
- package/src/testing/stub-ast-document-manager.ts +10 -24
- package/src/testing/stub-hydranium-text-documents.ts +29 -16
- package/src/testing/stub-model-service.ts +2 -2
package/src/langium/module.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { HydraniumLangiumProfiler } from './diagnostics/hydranium-langium-profil
|
|
|
15
15
|
import { type AstNode, type Module } from '@hydranium/langium';
|
|
16
16
|
import { type DefaultSharedModuleContext, type LangiumSharedServices, type PartialLangiumSharedServices } from '@hydranium/langium/lsp';
|
|
17
17
|
import { type TextDocument } from 'vscode-languageserver-textdocument';
|
|
18
|
+
import { type ClientSessionFactory, DefaultClientSessionFactory } from './model-service/client-session.js';
|
|
18
19
|
import { DefaultModelService, type ModelService } from './model-service/model-service.js';
|
|
19
20
|
import { type ProjectManager } from './project/project-manager.js';
|
|
20
21
|
import { SingleProjectManager } from './project/single-project-manager.js';
|
|
@@ -35,6 +36,7 @@ import { DefaultDocumentUriPolicy, type DocumentUriPolicy } from './workspace/do
|
|
|
35
36
|
import { HydraniumTextDocuments } from '../documents/hydranium-text-documents.js';
|
|
36
37
|
import { DefaultAstDocumentManager, type AstDocumentManager, type WritableFileSystemProvider } from '../documents/ast-document-manager.js';
|
|
37
38
|
import { DefaultSelfSaveRegistry, type SelfSaveRegistry } from '../documents/self-save-registry.js';
|
|
39
|
+
import { DefaultFileSystemTaskQueue, type FileSystemTaskQueue } from '../documents/file-system-task-queue.js';
|
|
38
40
|
import { DefaultEmptyFileSystemProvider } from './workspace/file-system-provider.js';
|
|
39
41
|
import { type ServerLanguageServices } from './language-module.js';
|
|
40
42
|
import { ExtendedServiceRegistry } from './service-registry.js';
|
|
@@ -177,7 +179,7 @@ export interface ServerAddedSharedServices<TProject extends Project = Project> {
|
|
|
177
179
|
* Tighten Langium's read-only `FileSystemProvider` slot to
|
|
178
180
|
* {@link WritableFileSystemProvider}. The framework's save path
|
|
179
181
|
* ({@link AstDocumentManager.save}, integrity corrections,
|
|
180
|
-
* `
|
|
182
|
+
* a session's `save`) requires write semantics — a read-only slot
|
|
181
183
|
* type forces an `as WritableFileSystemProvider` cast at the binding
|
|
182
184
|
* line and defers the failure of a read-only implementation to the
|
|
183
185
|
* first save. The `.`-entry default is
|
|
@@ -217,6 +219,12 @@ export interface ServerAddedSharedServices<TProject extends Project = Project> {
|
|
|
217
219
|
* behaviour rebind this slot with a subclass.
|
|
218
220
|
*/
|
|
219
221
|
AstDocumentManager: AstDocumentManager<AstNode>;
|
|
222
|
+
/**
|
|
223
|
+
* Orders the server's disk accesses of each file. One slot for every
|
|
224
|
+
* reader and writer: a service given a queue of its own does not order
|
|
225
|
+
* against the others.
|
|
226
|
+
*/
|
|
227
|
+
FileSystemTaskQueue: FileSystemTaskQueue;
|
|
220
228
|
/**
|
|
221
229
|
* Wires the framework's build-time features (integrity, AST
|
|
222
230
|
* enrichment) into Langium's build pipeline: owns the build-phase
|
|
@@ -273,6 +281,8 @@ export interface ServerAddedSharedServices<TProject extends Project = Project> {
|
|
|
273
281
|
model: {
|
|
274
282
|
TransferEncoder: TransferEncoder;
|
|
275
283
|
ModelService: ModelService<AstNode>;
|
|
284
|
+
/** Builds the handle of every session `ModelService.createSession` starts. */
|
|
285
|
+
ClientSessionFactory: ClientSessionFactory;
|
|
276
286
|
};
|
|
277
287
|
/**
|
|
278
288
|
* Shared contribution group for batch-level build-phase passes (see
|
|
@@ -507,6 +517,7 @@ export function createServerSharedModule(
|
|
|
507
517
|
return new DefaultEmptyFileSystemProvider(services);
|
|
508
518
|
},
|
|
509
519
|
AstDocumentManager: services => new DefaultAstDocumentManager(services),
|
|
520
|
+
FileSystemTaskQueue: services => new DefaultFileSystemTaskQueue(services),
|
|
510
521
|
// Eagerly constructed, so its build-phase listeners attach before the
|
|
511
522
|
// first build.
|
|
512
523
|
BuildPipelineIntegration: services => new DefaultBuildPipelineIntegration(services),
|
|
@@ -519,7 +530,8 @@ export function createServerSharedModule(
|
|
|
519
530
|
// Generic walker — adopters with a typed `$type → wire shape` overlay
|
|
520
531
|
// rebind this slot with a subclass.
|
|
521
532
|
TransferEncoder: services => new DefaultTransferEncoder(services),
|
|
522
|
-
ModelService: services => new DefaultModelService(services)
|
|
533
|
+
ModelService: services => new DefaultModelService(services),
|
|
534
|
+
ClientSessionFactory: services => new DefaultClientSessionFactory(services)
|
|
523
535
|
},
|
|
524
536
|
// Empty default so `services.buildPhasePasses` always resolves (Langium
|
|
525
537
|
// throws on access to an unbound slot). The framework's own integrity /
|
|
@@ -13,7 +13,7 @@ import { type RegistryItem } from '../../util/registry.js';
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* A single transform applied to the incoming transfer model on the structured
|
|
16
|
-
* (object) write path of `
|
|
16
|
+
* (object) write path of a client session's `update` / `save`, before
|
|
17
17
|
* serialisation. The RPC-update-stage sibling of `IntegrityRule` /
|
|
18
18
|
* `AstExtension` / `ValidationCheck` — same id-keyed,
|
|
19
19
|
* priority-ordered registry shape, but it runs on the transfer model rather
|
|
@@ -43,6 +43,15 @@ export const UNSUPPORTED_WRITE = defineMessage(
|
|
|
43
43
|
'Cannot write {uri}: the scheme names no location on disk'
|
|
44
44
|
);
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* A missing-file error shaped as a Node file system raises it, with code
|
|
48
|
+
* `ENOENT` and the path: callers tell a missing file from any other failure by
|
|
49
|
+
* those two fields, whichever provider is bound.
|
|
50
|
+
*/
|
|
51
|
+
function notFound(message: string, uri: URI): Error {
|
|
52
|
+
return Object.assign(new Error(message), { code: 'ENOENT', path: uri.fsPath });
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
/**
|
|
47
56
|
* File content keyed by path, for seeding an {@link InMemoryFileSystemProvider}.
|
|
48
57
|
*
|
|
@@ -162,7 +171,7 @@ export class InMemoryFileSystemProvider implements WritableFileSystemProvider {
|
|
|
162
171
|
}
|
|
163
172
|
const content = this.files.get(normalize(uri));
|
|
164
173
|
if (content === undefined) {
|
|
165
|
-
throw
|
|
174
|
+
throw notFound(NO_SUCH_FILE.format({ uri: uri.toString() }), uri);
|
|
166
175
|
}
|
|
167
176
|
return content;
|
|
168
177
|
}
|
|
@@ -192,7 +201,7 @@ export class InMemoryFileSystemProvider implements WritableFileSystemProvider {
|
|
|
192
201
|
if (this.hasChildren(path)) {
|
|
193
202
|
return { isFile: false, isDirectory: true, uri };
|
|
194
203
|
}
|
|
195
|
-
throw
|
|
204
|
+
throw notFound(NO_SUCH_PATH.format({ uri: uri.toString() }), uri);
|
|
196
205
|
}
|
|
197
206
|
|
|
198
207
|
exists(uri: URI): Promise<boolean> {
|
|
@@ -67,7 +67,7 @@ export function isInsideWriteLock(): boolean {
|
|
|
67
67
|
/**
|
|
68
68
|
* Thrown when the model facade is reached from inside a workspace write-lock
|
|
69
69
|
* holder — typically an integrity rule or build-phase pass writing through
|
|
70
|
-
* `
|
|
70
|
+
* a session's `update` / `save` or `ModelService.rebuild` during a build.
|
|
71
71
|
*
|
|
72
72
|
* Named rather than a bare `Error` because the remedy is specific and worth
|
|
73
73
|
* pointing at: either move the write out of the build (integrity rules that
|
|
@@ -7,31 +7,27 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { Debouncer, type Logger, ObservableValue, type MaybeObservableValue } from '@hydranium/protocol';
|
|
10
|
+
import { Debouncer, Deferred, type Logger, ObservableValue, type MaybeObservableValue } from '@hydranium/protocol';
|
|
11
11
|
import { URI, UriUtils } from '@hydranium/langium';
|
|
12
12
|
import { DefaultDocumentUpdateHandler } from '@hydranium/langium/lsp';
|
|
13
|
-
import { type HydraniumDocumentBuilder } from '../langium/document-builder/document-builder.js';
|
|
13
|
+
import { HYDRANIUM_BUILD_REASONS, type HydraniumDocumentBuilder } from '../langium/document-builder/document-builder.js';
|
|
14
14
|
import { type ServerSharedServices } from '../langium/module.js';
|
|
15
15
|
import { LANGUAGE_CLIENT_ID } from '../documents/client-ids.js';
|
|
16
16
|
import { type SelfSaveRegistry } from '../documents/self-save-registry.js';
|
|
17
17
|
import { type WritableFileSystemProvider } from '../documents/ast-document-manager.js';
|
|
18
18
|
import { type HydraniumTextDocuments } from '../documents/hydranium-text-documents.js';
|
|
19
19
|
import { isConnectionGoneError } from '../util/connection-liveness.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type DidChangeWatchedFilesParams,
|
|
22
|
+
type FileEvent,
|
|
23
|
+
FileChangeType,
|
|
24
|
+
type TextDocumentChangeEvent,
|
|
25
|
+
type TextDocumentWillSaveEvent,
|
|
26
|
+
type TextEdit
|
|
27
|
+
} from 'vscode-languageserver';
|
|
21
28
|
import { type TextDocument } from 'vscode-languageserver-textdocument';
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
* Default LSP-event reasons stamped by {@link HydraniumDocumentUpdateHandler}
|
|
25
|
-
* onto the next `documentBuilder.update` call via `markNextReason`. Exposed
|
|
26
|
-
* so adopters subclassing the handler can layer additional reasons or rename
|
|
27
|
-
* existing ones without losing the canonical names the framework emits.
|
|
28
|
-
*/
|
|
29
|
-
export const HYDRANIUM_BUILD_REASONS = Object.freeze({
|
|
30
|
-
didOpen: 'didOpen',
|
|
31
|
-
didChangeContent: 'didChangeContent',
|
|
32
|
-
didChangeWatchedFiles: 'didChangeWatchedFiles',
|
|
33
|
-
didClose: 'didClose'
|
|
34
|
-
} as const);
|
|
30
|
+
export { HYDRANIUM_BUILD_REASONS };
|
|
35
31
|
|
|
36
32
|
/**
|
|
37
33
|
* Construction options for {@link HydraniumDocumentUpdateHandler}.
|
|
@@ -57,6 +53,17 @@ export interface HydraniumDocumentUpdateHandlerOptions {
|
|
|
57
53
|
* load tests).
|
|
58
54
|
*/
|
|
59
55
|
readonly bypassNonLanguageClientChanges?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Longest an editor's save waits on the server, in milliseconds, at each of
|
|
58
|
+
* its two steps: the `willSaveWaitUntil` answer waits for the server's disk
|
|
59
|
+
* writes of the document already queued, and server writes queued after the
|
|
60
|
+
* answer wait for the editor's `didSave`. Either wait that runs out is
|
|
61
|
+
* logged, and the editor's write and the server's can then land in either
|
|
62
|
+
* order. Default `1000`: VS Code gives up on an answer after about 1.5 s,
|
|
63
|
+
* and switches the listener off for the session once four answers, over
|
|
64
|
+
* all documents, timed out or failed.
|
|
65
|
+
*/
|
|
66
|
+
readonly willSaveGateMs?: number;
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
/**
|
|
@@ -72,19 +79,25 @@ export interface HydraniumDocumentUpdateHandlerOptions {
|
|
|
72
79
|
* changes and watched-file changes bypass the debounce (their
|
|
73
80
|
* producers are already coalesced).
|
|
74
81
|
* 3. **Build-reason stamping** — the LSP event overrides
|
|
75
|
-
* (`didOpenDocument`, `didChangeContent`, `didChangeWatchedFiles
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
82
|
+
* (`didOpenDocument`, `didChangeContent`, `didChangeWatchedFiles`)
|
|
83
|
+
* populate {@link nextReason} with a canonical reason string.
|
|
84
|
+
* {@link fireDocumentUpdate} captures the reason; {@link dispatch} stages
|
|
85
|
+
* it on the builder via {@link HydraniumDocumentBuilder.markNextReason},
|
|
86
|
+
* so a subclass that formats build logs can tag its line with the event
|
|
87
|
+
* that caused the build without per-adopter wiring to capture it.
|
|
88
|
+
* 4. **Editor save gate** ({@link willSaveDocumentWaitUntil}) — the editor
|
|
89
|
+
* writes the file itself, so the server's disk writes of that document
|
|
90
|
+
* are ordered around it: the editor's save waits for the ones already
|
|
91
|
+
* queued, and the ones queued during its save wait for its `didSave`.
|
|
92
|
+
* Both waits are capped by
|
|
93
|
+
* {@link HydraniumDocumentUpdateHandlerOptions.willSaveGateMs}.
|
|
94
|
+
*
|
|
95
|
+
* The revert to disk after a document's last close is not dispatched here:
|
|
96
|
+
* the text store runs it for every head, the LSP head included, so a
|
|
97
|
+
* `didCloseDocument` added in a subclass would build the document twice. A
|
|
98
|
+
* change still debounced for the document is dropped when the store releases
|
|
99
|
+
* it: a `file:` document's revert rebuilds it, and a document of another
|
|
100
|
+
* scheme, which does not revert, keeps its last build.
|
|
88
101
|
*
|
|
89
102
|
* Adopters with their own handler subclass should extend this class
|
|
90
103
|
* (not Langium's `DefaultDocumentUpdateHandler`) so all of them are
|
|
@@ -133,6 +146,17 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
133
146
|
*/
|
|
134
147
|
protected pendingReason?: string;
|
|
135
148
|
|
|
149
|
+
protected readonly willSaveGateMs: number;
|
|
150
|
+
/**
|
|
151
|
+
* Per document URI, the release of the hold an editor's save in progress
|
|
152
|
+
* keeps on the document's disk queue. A second save of the document
|
|
153
|
+
* releases the first: its hold would otherwise keep the second's answer
|
|
154
|
+
* waiting until the cap. The key is the document, not the save, so for a
|
|
155
|
+
* client that sends its next `willSaveWaitUntil` before the previous
|
|
156
|
+
* `didSave`, that late `didSave` releases the next save's hold.
|
|
157
|
+
*/
|
|
158
|
+
protected readonly editorSaves = new Map<string, () => void>();
|
|
159
|
+
|
|
136
160
|
constructor(
|
|
137
161
|
protected readonly services: ServerSharedServices,
|
|
138
162
|
options: HydraniumDocumentUpdateHandlerOptions = {}
|
|
@@ -145,6 +169,19 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
145
169
|
this.debounce = ObservableValue.from(options.debounceMs ?? 0);
|
|
146
170
|
this.flushDebouncer = new Debouncer(services.Clock, () => this.flushPending(), { delayMs: this.debounce });
|
|
147
171
|
this.bypassNonLanguageClientChanges = options.bypassNonLanguageClientChanges ?? true;
|
|
172
|
+
this.willSaveGateMs = options.willSaveGateMs ?? 1000;
|
|
173
|
+
this.textDocuments.onDidSaveInLanguageClient(event => this.editorSaves.get(event.uri)?.());
|
|
174
|
+
// A change still debounced for a document the store has released would
|
|
175
|
+
// build it once more after the store's own revert: from disk again, or,
|
|
176
|
+
// for a file that never existed, by reading a file that is not there. A
|
|
177
|
+
// document of another scheme does not revert, so dropping its change
|
|
178
|
+
// leaves it at its last build.
|
|
179
|
+
this.textDocuments.onDidCloseLastOpen(event => {
|
|
180
|
+
this.pendingChanged.delete(URI.parse(event.uri).toString());
|
|
181
|
+
if (this.pendingChanged.size === 0 && this.pendingDeleted.size === 0) {
|
|
182
|
+
this.pendingReason = undefined;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
148
185
|
}
|
|
149
186
|
|
|
150
187
|
didOpenDocument(_change: TextDocumentChangeEvent<TextDocument>): void {
|
|
@@ -169,70 +206,6 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
169
206
|
super.didChangeContent(change);
|
|
170
207
|
}
|
|
171
208
|
|
|
172
|
-
/**
|
|
173
|
-
* Last-client close is the missing rebuild trigger in Langium's
|
|
174
|
-
* `DefaultDocumentUpdateHandler`: `documents.onDidClose` fires on every
|
|
175
|
-
* per-client close, but the default handler ignores the event entirely.
|
|
176
|
-
* For multi-client servers this leaves the LangiumDocument carrying the
|
|
177
|
-
* unsaved in-memory `textDocument` of the last writer until something
|
|
178
|
-
* else triggers a rebuild — integrity rules walking `langiumDocuments.all`
|
|
179
|
-
* then see stale text long after the user discarded their edits.
|
|
180
|
-
*
|
|
181
|
-
* We funnel the last-client transition through the same dispatch path
|
|
182
|
-
* the other LSP events use, so the rebuild gets debouncing, workspace
|
|
183
|
-
* locking, build-reason stamping, and `markNextReason` for free. The
|
|
184
|
-
* scheme split governs the `DocumentBuilder.update` shape:
|
|
185
|
-
*
|
|
186
|
-
* - **`file:` URI** → `update([uri], [])`. Routes through
|
|
187
|
-
* `LangiumDocuments.invalidateDocument` (doc stays in the trie) and
|
|
188
|
-
* `LangiumDocumentFactory.update`, which consults the LSP-tracked
|
|
189
|
-
* open-documents map first. That map is cleared LATER in the same
|
|
190
|
-
* synchronous `notifyDidCloseTextDocument` call that fires this event,
|
|
191
|
-
* and {@link dispatch} defers the build behind `workspaceManager.ready`
|
|
192
|
-
* — so by the time the factory looks, the URI is gone: it falls back to
|
|
193
|
-
* `FileSystemProvider.readFile` and the doc rebuilds from disk,
|
|
194
|
-
* Langium's canonical close-without-save semantic. A dispatch made
|
|
195
|
-
* synchronously here would still see the open document and re-read the
|
|
196
|
-
* in-memory text instead.
|
|
197
|
-
* - **non-`file:` URI** (`builtin:`, `untitled:`) → no dispatch.
|
|
198
|
-
* Adopter-loaded schemes — documents pulled in via
|
|
199
|
-
* `loadAdditionalDocuments` — need to stay in the workspace index;
|
|
200
|
-
* dropping them on close would break every cross-reference that
|
|
201
|
-
* targets them. The framework cannot enumerate adopter schemes, so the
|
|
202
|
-
* safe default is "leave alone unless it's a file." Adopters that need
|
|
203
|
-
* ephemeral-URI cleanup handle it explicitly.
|
|
204
|
-
*
|
|
205
|
-
* Per-client closes (where other clients still hold the URI) are
|
|
206
|
-
* suppressed via {@link HydraniumTextDocuments.isOpenInAnyClient}.
|
|
207
|
-
*
|
|
208
|
-
* The dispatch waits for the URI's disk queue to drain, because the rebuild
|
|
209
|
-
* re-reads the file: dispatched while a save is still queued, it reads the
|
|
210
|
-
* text from before that save, and the document reverts past it. A client
|
|
211
|
-
* that opens the URI meanwhile holds the text, so no revert follows.
|
|
212
|
-
*/
|
|
213
|
-
didCloseDocument(change: TextDocumentChangeEvent<TextDocument>): void {
|
|
214
|
-
if (this.textDocuments.isOpenInAnyClient(change.document.uri)) {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
const uri = URI.parse(change.document.uri);
|
|
218
|
-
if (uri.scheme !== 'file') {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
this.services.workspace.AstDocumentManager.queueDiskTask(change.document.uri, async () => undefined)
|
|
222
|
-
.then(() => {
|
|
223
|
-
if (this.textDocuments.isOpenInAnyClient(change.document.uri)) {
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
this.nextReason = HYDRANIUM_BUILD_REASONS.didClose;
|
|
227
|
-
this.immediateFlush = true;
|
|
228
|
-
this.fireDocumentUpdate([uri], []);
|
|
229
|
-
})
|
|
230
|
-
.catch((err: unknown) => {
|
|
231
|
-
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
232
|
-
this.logger.error(`Revert on last close dropped for ${change.document.uri}. ${detail}`);
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
209
|
override didChangeWatchedFiles(params: DidChangeWatchedFilesParams): void {
|
|
237
210
|
void this.filterSelfSaves(params)
|
|
238
211
|
.then(filtered => {
|
|
@@ -260,6 +233,79 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
260
233
|
});
|
|
261
234
|
}
|
|
262
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Answer an editor's `willSaveWaitUntil` with no edits once the server's
|
|
238
|
+
* disk writes of the document already queued have landed, or after
|
|
239
|
+
* {@link willSaveGateMs}, whichever is first. From the moment the queue
|
|
240
|
+
* reaches this save, disk tasks of the document queued behind it wait for
|
|
241
|
+
* the editor's `didSave`, reported through
|
|
242
|
+
* `HydraniumTextDocuments.onDidSaveInLanguageClient`, or for
|
|
243
|
+
* {@link willSaveGateMs} more.
|
|
244
|
+
*
|
|
245
|
+
* The hold is a disk task that waits only for that signal or its timer:
|
|
246
|
+
* waiting on a build, a save or anything else queued for the document would
|
|
247
|
+
* wedge its queue. Resolves in every case, since a failed request counts
|
|
248
|
+
* against the listener in VS Code; Langium drops the request's cancellation,
|
|
249
|
+
* so an abandoned request still runs to the cap. A hold that runs out is
|
|
250
|
+
* logged at debug only: an editor skips `didSave` for a save it cancels or
|
|
251
|
+
* that changed nothing.
|
|
252
|
+
*/
|
|
253
|
+
willSaveDocumentWaitUntil(event: TextDocumentWillSaveEvent<TextDocument>): Promise<TextEdit[]> {
|
|
254
|
+
const uri = event.document.uri;
|
|
255
|
+
this.editorSaves.get(uri)?.();
|
|
256
|
+
const hold = new Deferred();
|
|
257
|
+
const release = (): void => {
|
|
258
|
+
if (this.editorSaves.get(uri) === release) {
|
|
259
|
+
this.editorSaves.delete(uri);
|
|
260
|
+
}
|
|
261
|
+
hold.resolve();
|
|
262
|
+
};
|
|
263
|
+
this.editorSaves.set(uri, release);
|
|
264
|
+
return new Promise<TextEdit[]>(resolve => {
|
|
265
|
+
const clock = this.services.Clock;
|
|
266
|
+
const cap = clock.setTimer(() => {
|
|
267
|
+
this.logger.warn(
|
|
268
|
+
`willSaveWaitUntil for ${uri} answered after ${this.willSaveGateMs} ms with server writes of it still queued; the editor's save may race them.`
|
|
269
|
+
);
|
|
270
|
+
resolve([]);
|
|
271
|
+
}, this.willSaveGateMs);
|
|
272
|
+
try {
|
|
273
|
+
void this.services.workspace.FileSystemTaskQueue.enqueue(uri, async () => {
|
|
274
|
+
cap.dispose();
|
|
275
|
+
resolve([]);
|
|
276
|
+
const bound = clock.setTimer(() => {
|
|
277
|
+
this.logger.debug(
|
|
278
|
+
`No didSave for ${uri} within ${this.willSaveGateMs} ms; server writes of it no longer wait for the editor's save.`
|
|
279
|
+
);
|
|
280
|
+
release();
|
|
281
|
+
}, this.willSaveGateMs);
|
|
282
|
+
await hold.promise;
|
|
283
|
+
bound.dispose();
|
|
284
|
+
});
|
|
285
|
+
} catch (err) {
|
|
286
|
+
cap.dispose();
|
|
287
|
+
this.logger.error(
|
|
288
|
+
`willSaveWaitUntil for ${uri} answered without waiting. ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`
|
|
289
|
+
);
|
|
290
|
+
release();
|
|
291
|
+
resolve([]);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Present so that Langium advertises `save` and editors send `didSave`;
|
|
298
|
+
* without it every hold of {@link willSaveDocumentWaitUntil} waits out its
|
|
299
|
+
* cap. The hold is released on
|
|
300
|
+
* `HydraniumTextDocuments.onDidSaveInLanguageClient` instead: it fires as
|
|
301
|
+
* soon as the editor reports the save, while `onDidSave`, and so this
|
|
302
|
+
* method, follows only once the file has been read back, and not at all
|
|
303
|
+
* when the file does not hold the store's text.
|
|
304
|
+
*/
|
|
305
|
+
didSaveDocument(_event: TextDocumentChangeEvent<TextDocument>): void {
|
|
306
|
+
// Nothing to do; see the doc comment.
|
|
307
|
+
}
|
|
308
|
+
|
|
263
309
|
/**
|
|
264
310
|
* Drop watched-file changes whose path + mtime match a recent self-save
|
|
265
311
|
* record. Deletions always pass (no mtime); stat failures pass (the
|
package/src/lsp/shared-module.ts
CHANGED
|
@@ -28,14 +28,10 @@ import { HydraniumDocumentUpdateHandler } from './hydranium-document-update-hand
|
|
|
28
28
|
* (an output channel, a structured logger) override the slot after this
|
|
29
29
|
* module.
|
|
30
30
|
*
|
|
31
|
-
* - {@link HydraniumDocumentUpdateHandler}
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* client closes a document.
|
|
36
|
-
* Adopters with their own update-handler subclass should extend
|
|
37
|
-
* `HydraniumDocumentUpdateHandler` (not Langium's
|
|
38
|
-
* `DefaultDocumentUpdateHandler`) to preserve those behaviours.
|
|
31
|
+
* - {@link HydraniumDocumentUpdateHandler} carries the framework's handling of
|
|
32
|
+
* the LSP document events. Adopters with their own update-handler subclass
|
|
33
|
+
* should extend it (not Langium's `DefaultDocumentUpdateHandler`) to keep
|
|
34
|
+
* that handling.
|
|
39
35
|
*
|
|
40
36
|
* **Why this is a shared slot and not a per-language one.** Langium declares
|
|
41
37
|
* `DocumentUpdateHandler` in `LangiumSharedLSPServices` — there is exactly one
|
|
@@ -22,10 +22,9 @@ import { guardDiagnosticsConnection } from './diagnostics-connection.js';
|
|
|
22
22
|
* SHARED slot, so the head binds it in `createLspServerSharedModule` — a
|
|
23
23
|
* separate module from `createLspServerLanguageModule`. An adopter who adds
|
|
24
24
|
* only the language module gets a server that boots, links and serves
|
|
25
|
-
* completion, and quietly runs Langium's `DefaultDocumentUpdateHandler`:
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* with nothing in the log to say so.
|
|
25
|
+
* completion, and quietly runs Langium's `DefaultDocumentUpdateHandler`: every
|
|
26
|
+
* behaviour `HydraniumDocumentUpdateHandler` adds, the editor save gate
|
|
27
|
+
* included, is absent, with nothing in the log to say so.
|
|
29
28
|
*
|
|
30
29
|
* The type system does not catch it. `createIntegrationServices` returns
|
|
31
30
|
* `inject(...) as unknown as TShared`, so widening `TShared` with
|
|
@@ -23,12 +23,18 @@ import { DefaultServerLocale, type ServerLocale } from '../locale/server-locale.
|
|
|
23
23
|
import { DefaultMessageRenderer, type MessageRenderer } from '../messages/renderer.js';
|
|
24
24
|
import type { Harness } from '@hydranium/protocol/testing';
|
|
25
25
|
import { type AstNode, type AstNodeDescription, type WorkspaceLock } from '@hydranium/langium';
|
|
26
|
+
import {
|
|
27
|
+
type ClientSessionFactory,
|
|
28
|
+
type ClientSessionFactoryOptions,
|
|
29
|
+
DefaultClientSessionFactory
|
|
30
|
+
} from '../langium/model-service/client-session.js';
|
|
26
31
|
import type { ModelService, ModelServiceOptions } from '../langium/model-service/model-service.js';
|
|
27
32
|
import type { ServerSharedServices } from '../langium/module.js';
|
|
28
33
|
import type { AstDiagnostic } from '../langium/validation/document-validator.js';
|
|
29
34
|
import { DefaultTransferEncoder, type TransferEncoder } from '../langium/transfer/transfer-encoder.js';
|
|
30
35
|
import { DefaultDocumentUriPolicy, type DocumentUriPolicy } from '../langium/workspace/document-uri-policy.js';
|
|
31
36
|
import { HydraniumWorkspaceLock } from '../langium/workspace/hydranium-workspace-lock.js';
|
|
37
|
+
import { DefaultFileSystemTaskQueue, type FileSystemTaskQueue } from '../documents/file-system-task-queue.js';
|
|
32
38
|
import type { FakeDocumentOptions } from './fake-document.js';
|
|
33
39
|
import { makeStubDocumentBuilder, type StubDocumentBuilder } from './stub-document-builder.js';
|
|
34
40
|
import { makeStubIndexManager, type StubIndexManager } from './stub-index-manager.js';
|
|
@@ -85,6 +91,8 @@ export interface TestSharedServices<
|
|
|
85
91
|
SelfSaveRegistry: StubSelfSaveRegistry;
|
|
86
92
|
ProjectManager: StubProjectManager<TProject>;
|
|
87
93
|
DocumentUriPolicy: DocumentUriPolicy;
|
|
94
|
+
/** The REAL queue: a stub that runs each task at once hides the save interleaving the queue exists to prevent. */
|
|
95
|
+
FileSystemTaskQueue: FileSystemTaskQueue;
|
|
88
96
|
/**
|
|
89
97
|
* Present only when {@link MakeTestServicesOptions.seedIndex} was passed.
|
|
90
98
|
* Omitted otherwise for the same reason as `ServiceRegistry` — see that
|
|
@@ -105,6 +113,7 @@ export interface TestSharedServices<
|
|
|
105
113
|
readonly model: {
|
|
106
114
|
TransferEncoder: TransferEncoder<TTransferDiagnostic>;
|
|
107
115
|
ModelService: ModelService<TAst, TDiagnostic, TTransfer>;
|
|
116
|
+
ClientSessionFactory: ClientSessionFactory;
|
|
108
117
|
};
|
|
109
118
|
/**
|
|
110
119
|
* The REAL services, not stubs — the framework renderer's no-catalogue
|
|
@@ -151,7 +160,7 @@ export interface MakeTestServicesOptions<
|
|
|
151
160
|
*
|
|
152
161
|
* **Default: no `ServiceRegistry` slot at all.** Binding an empty registry
|
|
153
162
|
* by default would silently change behaviour for the tests that rely on the
|
|
154
|
-
* slot being absent — `ModelService.
|
|
163
|
+
* slot being absent — `ModelService.rewriteModel` optional-chains it
|
|
155
164
|
* precisely for those bundles — so a registry appears only when a test asks
|
|
156
165
|
* for one.
|
|
157
166
|
*/
|
|
@@ -209,11 +218,18 @@ export interface MakeTestServicesOptions<
|
|
|
209
218
|
modelService?: (services: ServerSharedServices<TProject>) => ModelService<TAst, TDiagnostic, TTransfer>;
|
|
210
219
|
/**
|
|
211
220
|
* Framework {@link ModelServiceOptions} for the DEFAULT stub service, so a
|
|
212
|
-
* test can reach an option-gated path (`serializeBuilds
|
|
213
|
-
*
|
|
214
|
-
*
|
|
221
|
+
* test can reach an option-gated path (`serializeBuilds`) without supplying
|
|
222
|
+
* a whole {@link modelService} factory. Ignored when `modelService` is
|
|
223
|
+
* given — that factory owns its own construction.
|
|
215
224
|
*/
|
|
216
225
|
modelServiceOptions?: ModelServiceOptions;
|
|
226
|
+
/**
|
|
227
|
+
* Options of the {@link DefaultClientSessionFactory} bound on
|
|
228
|
+
* `model.ClientSessionFactory`, so a test can reach the slow-update warn.
|
|
229
|
+
* Its sessions serialise and rebuild through the `model.ModelService` slot,
|
|
230
|
+
* so a test of a model service subclass passes it as {@link modelService}.
|
|
231
|
+
*/
|
|
232
|
+
clientSessionFactoryOptions?: ClientSessionFactoryOptions;
|
|
217
233
|
/**
|
|
218
234
|
* Override the {@link TransferEncoder} factory. Default: framework
|
|
219
235
|
* {@link TransferEncoder} with no overrides.
|
|
@@ -326,7 +342,7 @@ export function makeTestServices<
|
|
|
326
342
|
// is dependency-free (as `HydraniumWorkspaceLock` is) — a no-op stub of a
|
|
327
343
|
// synchronisation primitive hides the very interleaving it exists to prevent.
|
|
328
344
|
//
|
|
329
|
-
// `model
|
|
345
|
+
// The `model` slots are populated below
|
|
330
346
|
// (after the factories run); the literal uses unsafe casts to
|
|
331
347
|
// partially-built objects to satisfy TestSharedServices, then patches
|
|
332
348
|
// them in.
|
|
@@ -350,7 +366,8 @@ export function makeTestServices<
|
|
|
350
366
|
// Spread for the same reason as `ServiceRegistry` above — the slot must
|
|
351
367
|
// be genuinely ABSENT, not present-and-undefined, when no index was seeded.
|
|
352
368
|
...(indexManager ? { IndexManager: indexManager } : {}),
|
|
353
|
-
WorkspaceLock: new HydraniumWorkspaceLock()
|
|
369
|
+
WorkspaceLock: new HydraniumWorkspaceLock(),
|
|
370
|
+
FileSystemTaskQueue: new DefaultFileSystemTaskQueue({ workspace: { DocumentUriPolicy: documentUriPolicy } })
|
|
354
371
|
},
|
|
355
372
|
model: {} as TestSharedServices<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic>['model'],
|
|
356
373
|
ServerLocale: {} as ServerLocale,
|
|
@@ -380,9 +397,11 @@ export function makeTestServices<
|
|
|
380
397
|
const mutableModel = services.model as {
|
|
381
398
|
TransferEncoder: TransferEncoder<TTransferDiagnostic>;
|
|
382
399
|
ModelService: ModelService<TAst, TDiagnostic, TTransfer>;
|
|
400
|
+
ClientSessionFactory: ClientSessionFactory;
|
|
383
401
|
};
|
|
384
402
|
mutableModel.TransferEncoder = transferEncoder;
|
|
385
403
|
mutableModel.ModelService = modelService;
|
|
404
|
+
mutableModel.ClientSessionFactory = new DefaultClientSessionFactory(sharedServices, options.clientSessionFactoryOptions);
|
|
386
405
|
|
|
387
406
|
return {
|
|
388
407
|
services: sharedServices,
|
|
@@ -301,7 +301,7 @@ export interface LspServerConnection extends Harness {
|
|
|
301
301
|
*
|
|
302
302
|
* **An egress push is asynchronous well past the call that triggers it** —
|
|
303
303
|
* the framework coalesces per URI and drives the sync from a build-phase
|
|
304
|
-
* settle, so a `
|
|
304
|
+
* settle, so a session's `update` resolves long before the push is sent.
|
|
305
305
|
* Record `appliedEdits.length`, drive the edit, then await this; reading the
|
|
306
306
|
* capture on the line after the write proves nothing.
|
|
307
307
|
*/
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Any test that drives a **write path** needs one, and the reason is not
|
|
16
16
|
* obvious enough to rediscover per suite. The writes themselves are usually
|
|
17
|
-
* in-memory — `
|
|
17
|
+
* in-memory — a session's `update` goes to the multi-client text store, and
|
|
18
18
|
* only `save` reaches disk — but a rebuild also runs the integrity rules, and
|
|
19
19
|
* their default `'silent'` sync mode persists repairs through
|
|
20
20
|
* `WritableFileSystemProvider.writeFile`. So a write-path test pointed at a
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Driver for the structured-write transform chain — the rewrite registry plus
|
|
12
|
-
* the serializer — run the way `
|
|
12
|
+
* the serializer — run the way a session's `update` runs it.
|
|
13
13
|
*
|
|
14
14
|
* # Why a driver and not a documented recipe
|
|
15
15
|
*
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* This runs rewrite → serialize and stops. It does NOT open the document, push
|
|
39
39
|
* text into the multi-client store, drive a build or return an
|
|
40
40
|
* {@link AstDocument}: those need a live workspace, and a test that wants them
|
|
41
|
-
* should call `
|
|
41
|
+
* should call a session's `update` itself rather than a driver standing in for
|
|
42
42
|
* it. What is left is the part with no observable of its own — the text and the
|
|
43
43
|
* rewritten model — which is why it needed lifting.
|
|
44
44
|
*
|
|
@@ -61,7 +61,7 @@ export interface UpdatePipelineArgs<TTransfer extends TransferElement = Transfer
|
|
|
61
61
|
* rather than merely labelling it.
|
|
62
62
|
*/
|
|
63
63
|
readonly uri: string;
|
|
64
|
-
/** Transfer-model payload, as an adopter caller would hand it to `
|
|
64
|
+
/** Transfer-model payload, as an adopter caller would hand it to a session's `update`. */
|
|
65
65
|
readonly model: TTransfer;
|
|
66
66
|
/**
|
|
67
67
|
* Previous AST root threaded into every rewrite, so a diff-based rewrite can
|
|
@@ -90,7 +90,7 @@ export interface UpdatePipelineResult<TTransfer extends TransferElement = Transf
|
|
|
90
90
|
* surface but it makes the precise assertion unavailable.
|
|
91
91
|
*/
|
|
92
92
|
readonly rewritten: TTransfer;
|
|
93
|
-
/** The serialized text, which is what `
|
|
93
|
+
/** The serialized text, which is what a session's `update` applies to the store. */
|
|
94
94
|
readonly text: string;
|
|
95
95
|
}
|
|
96
96
|
|