@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
|
@@ -8,21 +8,13 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
type BasedOn,
|
|
12
11
|
type CanonicalUri,
|
|
13
|
-
type CloseModelArgs,
|
|
14
|
-
ConflictError,
|
|
15
|
-
isSnapshotVersion,
|
|
16
12
|
defineMessage,
|
|
17
|
-
Logger,
|
|
18
13
|
type MaybeObservableValue,
|
|
19
14
|
type MaybePromise,
|
|
20
15
|
ObservableValue,
|
|
21
16
|
type TransferElement,
|
|
22
|
-
type
|
|
23
|
-
type Tracer,
|
|
24
|
-
type TransferSaveArgs,
|
|
25
|
-
type TransferUpdateArgs
|
|
17
|
+
type Tracer
|
|
26
18
|
} from '@hydranium/protocol';
|
|
27
19
|
import { type AstNode, DocumentState, type LangiumDocument, UriUtils, type URI } from '@hydranium/langium';
|
|
28
20
|
import { type AstDiagnostic } from '../validation/document-validator.js';
|
|
@@ -35,10 +27,9 @@ import { type LogNameOptions } from '../diagnostics/logger.js';
|
|
|
35
27
|
import { IntegrityService } from '../integrity/integrity-service.js';
|
|
36
28
|
import { labelPhaseListener } from '../document-builder/labeled-phase-listener.js';
|
|
37
29
|
import { LANGUAGE_CLIENT_ID } from '../../documents/client-ids.js';
|
|
38
|
-
import { DocumentNotOpenError } from '../../documents/client-session-errors.js';
|
|
39
30
|
import { type OpenOptions } from '../../documents/client-session-registry.js';
|
|
40
31
|
import { type ServerSharedServices } from '../module.js';
|
|
41
|
-
import { type ClientSession
|
|
32
|
+
import { type ClientSession } from './client-session.js';
|
|
42
33
|
|
|
43
34
|
/**
|
|
44
35
|
* The undo-stack entry for a server-authored write pushed to the editor.
|
|
@@ -74,29 +65,6 @@ class SaveSettleTimeoutError extends Error {}
|
|
|
74
65
|
* and the defaults are the behaviour described on each one.
|
|
75
66
|
*/
|
|
76
67
|
export interface ModelServiceOptions extends LogNameOptions {
|
|
77
|
-
/**
|
|
78
|
-
* When set, {@link ModelService.update} logs a `warn` line if its
|
|
79
|
-
* end-to-end wait (serialise + content-change apply + rebuild +
|
|
80
|
-
* settled-phase wait) exceeds this many milliseconds. Default
|
|
81
|
-
* `undefined` (no warn line ever emitted; the underlying
|
|
82
|
-
* `Logger.time` debug timing log is unchanged either way).
|
|
83
|
-
*
|
|
84
|
-
* Pure observability — does NOT abort the update, does NOT change
|
|
85
|
-
* resolution semantics. Adopters wanting a hard timeout that throws
|
|
86
|
-
* instead override {@link ModelService.update} on their subclass and
|
|
87
|
-
* race the parent call against their own deadline.
|
|
88
|
-
*
|
|
89
|
-
* Recommended starting threshold: 2-5 seconds for interactive paths
|
|
90
|
-
* (form save, diagram edit). Workspaces with very large documents
|
|
91
|
-
* or slow validation may legitimately exceed 5s on the cold path —
|
|
92
|
-
* tune per workspace.
|
|
93
|
-
*
|
|
94
|
-
* Accepts a {@link MaybeObservableValue} so the threshold can be a
|
|
95
|
-
* fixed constant or bound to a user setting via `Settings.number`
|
|
96
|
-
* and retuned live. Leaving it unset disables the warn line entirely
|
|
97
|
-
* (and skips the per-update stopwatch).
|
|
98
|
-
*/
|
|
99
|
-
readonly slowUpdateWarnMs?: MaybeObservableValue<number>;
|
|
100
68
|
/**
|
|
101
69
|
* Serialise the facade's own build under the workspace WRITE lock, the way
|
|
102
70
|
* Langium's `DefaultDocumentUpdateHandler` dispatches its build. Default
|
|
@@ -119,9 +87,9 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
119
87
|
* option removes is the wasted pass, not just its visible symptom.
|
|
120
88
|
*
|
|
121
89
|
* **What `false` buys.** `WorkspaceLock` is not reentrant. A caller that
|
|
122
|
-
* reaches `update` / `save`
|
|
123
|
-
* an integrity rule or build-phase pass that writes
|
|
124
|
-
* deadlocks: acquiring the lock cancels the running holder, and the new
|
|
90
|
+
* reaches a session's `update` / `save` or `rebuild` while already holding
|
|
91
|
+
* the write lock — an integrity rule or build-phase pass that writes
|
|
92
|
+
* back — deadlocks: acquiring the lock cancels the running holder, and the new
|
|
125
93
|
* acquisition then waits for that holder to release while the holder waits
|
|
126
94
|
* for this call. On `true` that shape is DETECTED and rejected with
|
|
127
95
|
* {@link ReentrantWriteLockError} rather than hanging, wherever a host
|
|
@@ -144,10 +112,11 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
144
112
|
*
|
|
145
113
|
* In-process facade over the framework's document plumbing
|
|
146
114
|
* (`HydraniumTextDocuments`, `LangiumDocuments`,
|
|
147
|
-
* `DocumentBuilder`, `WritableFileSystemProvider`). Owns the
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* doesn't have to be re-implemented per
|
|
115
|
+
* `DocumentBuilder`, `WritableFileSystemProvider`). Owns the document
|
|
116
|
+
* lifecycle the data-server and GLSP heads delegate to — client sessions that
|
|
117
|
+
* open, update, save and close documents, the phase reads, and `ready` — so
|
|
118
|
+
* coordinating those primitives doesn't have to be re-implemented per head.
|
|
119
|
+
* Every open and write goes through a session from {@link createSession}.
|
|
151
120
|
*
|
|
152
121
|
* "Model" here means the parsed AST — distinct from the wire-shape
|
|
153
122
|
* `TransferDocument` in `@hydranium/protocol`.
|
|
@@ -157,9 +126,9 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
157
126
|
* Multiple in-process consumers want the same workspace-level
|
|
158
127
|
* operations:
|
|
159
128
|
* - The data-server head turns these into typed RPC methods.
|
|
160
|
-
* - The GLSP head uses the same lifecycle for diagram-driven edits
|
|
161
|
-
*
|
|
162
|
-
*
|
|
129
|
+
* - The GLSP head uses the same lifecycle for diagram-driven edits: GModel
|
|
130
|
+
* operation handlers write through the diagram's session and wait on phases
|
|
131
|
+
* before reading.
|
|
163
132
|
* - Server-internal callers (integrity service, ad-hoc bridges, tests)
|
|
164
133
|
* want the same operations without the wire serialisation step.
|
|
165
134
|
*
|
|
@@ -172,15 +141,16 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
172
141
|
*
|
|
173
142
|
* **Read-latest supersession**
|
|
174
143
|
*
|
|
175
|
-
*
|
|
144
|
+
* A session's default `update` and `save` use Langium's per-URI
|
|
176
145
|
* `DocumentBuilder.waitUntil` to wait for the integrity-settled landmark
|
|
177
146
|
* ({@link IntegrityService.SettledState}), then read the post-build state. Concurrent
|
|
178
147
|
* in-process callers on the same URI all see the latest post-build
|
|
179
148
|
* snapshot — none deadlock waiting for a specific version's settled
|
|
180
149
|
* event. Adopters wanting strict version-matched semantics (resolve
|
|
181
150
|
* with vN's snapshot specifically, log "vN superseded by vM at vN+1")
|
|
182
|
-
* override
|
|
183
|
-
* version checks; the framework default doesn't need it for
|
|
151
|
+
* override `DefaultClientSession.updateDocument` to attach a phase-listener
|
|
152
|
+
* with explicit version checks; the framework default doesn't need it for
|
|
153
|
+
* safety.
|
|
184
154
|
*
|
|
185
155
|
* **Returns AST snapshots, not wire envelopes**
|
|
186
156
|
*
|
|
@@ -232,7 +202,7 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
232
202
|
* **Not the `TransferEncoder`'s parameter of the same name**, which is
|
|
233
203
|
* that encoder's OUTPUT and so names the wire shape. This one names its
|
|
234
204
|
* input, and an adopter binds the two to different types.
|
|
235
|
-
* - `TTransfer` — transfer-model root
|
|
205
|
+
* - `TTransfer` — transfer-model root a session's `update` / `save` accept
|
|
236
206
|
* args. Constrained to {@link TransferElement}. Defaults to the
|
|
237
207
|
* structural base.
|
|
238
208
|
*/
|
|
@@ -266,13 +236,13 @@ export interface ModelService<
|
|
|
266
236
|
indexed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
|
|
267
237
|
validated(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
268
238
|
|
|
269
|
-
update(args: TransferUpdateArgs<TTransfer>, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
270
|
-
save(args: TransferSaveArgs<TTransfer>, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
271
|
-
|
|
272
|
-
open(args: OpenModelArgs): Promise<Disposable>;
|
|
273
|
-
close(args: CloseModelArgs): Promise<void>;
|
|
274
239
|
isOpen(uri: string): boolean;
|
|
275
240
|
snapshot(uri: string): AstDocument<TAst, TDiagnostic> | undefined;
|
|
241
|
+
/**
|
|
242
|
+
* The text a session's write of `model` to `uri` applies: a textual model
|
|
243
|
+
* as given, a structured one rewritten and serialised.
|
|
244
|
+
*/
|
|
245
|
+
modelToText(uri: string, model: TTransfer | string, cancelToken?: CancellationToken): Promise<string>;
|
|
276
246
|
getDocument(uri: string): LangiumDocument | undefined;
|
|
277
247
|
|
|
278
248
|
onModelUpdated(uri: string, listener: (event: AstDocumentUpdatedEvent<TAst, TDiagnostic>) => void): Disposable;
|
|
@@ -280,12 +250,17 @@ export interface ModelService<
|
|
|
280
250
|
onClientClosed(uri: string, clientId: string, listener: () => void): Disposable;
|
|
281
251
|
|
|
282
252
|
/**
|
|
283
|
-
* Start a client session
|
|
284
|
-
* a
|
|
285
|
-
*
|
|
253
|
+
* Start a client session, the only way to open and write documents through
|
|
254
|
+
* this service. Pass a `label` naming the participant; without one it is
|
|
255
|
+
* `session`. The id defaults to `label#` plus a random UUID; a fixed
|
|
256
|
+
* `clientId` is taken as given. Throws `DuplicateClientIdError` when the id
|
|
257
|
+
* is reserved by the framework, held by another live session, or has
|
|
286
258
|
* documents open under it as a client that is not a session.
|
|
287
259
|
*
|
|
288
|
-
* `TOpenOptions` types the options the session's `open` takes
|
|
260
|
+
* `TOpenOptions` types the options the session's `open` takes and its
|
|
261
|
+
* `openOptions` returns. The narrowing is an unchecked cast, and it holds
|
|
262
|
+
* only for the caller that started the session: a holder reached through
|
|
263
|
+
* {@link getSession} sees plain `OpenOptions`.
|
|
289
264
|
*/
|
|
290
265
|
createSession<TOpenOptions extends OpenOptions = OpenOptions>(
|
|
291
266
|
label?: string,
|
|
@@ -299,7 +274,7 @@ export class DefaultModelService<
|
|
|
299
274
|
TAst extends AstNode,
|
|
300
275
|
TDiagnostic extends AstDiagnostic = AstDiagnostic,
|
|
301
276
|
/**
|
|
302
|
-
* Structured payload
|
|
277
|
+
* Structured payload a session's `update` / `save` accept. Constrained to
|
|
303
278
|
* {@link TransferElement} — the minimal `{ readonly $type: string }`
|
|
304
279
|
* shape. Both transfer-model overlay roots (which extend
|
|
305
280
|
* `TransferElement` explicitly) and AST root types (which have
|
|
@@ -319,12 +294,6 @@ export class DefaultModelService<
|
|
|
319
294
|
* (see {@link DocumentUriPolicy}).
|
|
320
295
|
*/
|
|
321
296
|
protected readonly uriPolicy: DocumentUriPolicy;
|
|
322
|
-
/**
|
|
323
|
-
* Live slow-update-warn threshold cell, or `undefined` when the option
|
|
324
|
-
* was not supplied (warn line + stopwatch disabled). Reads `.value` per
|
|
325
|
-
* update so a setting-bound threshold retunes without reconstruction.
|
|
326
|
-
*/
|
|
327
|
-
protected readonly slowUpdateWarn?: ObservableValue<number>;
|
|
328
297
|
/** See {@link ModelServiceOptions.serializeBuilds}. Defaults to `true`. */
|
|
329
298
|
protected readonly serializeBuilds: ObservableValue<boolean>;
|
|
330
299
|
|
|
@@ -337,11 +306,7 @@ export class DefaultModelService<
|
|
|
337
306
|
protected readonly syncChains = new Map<string, Promise<void>>();
|
|
338
307
|
protected readonly pendingSync = new Map<string, string>();
|
|
339
308
|
|
|
340
|
-
/**
|
|
341
|
-
* The live sessions this service started, by client id. {@link update}
|
|
342
|
-
* reads it to tell a session's write, which must find its document open,
|
|
343
|
-
* from a write under a plain client id, which opens it.
|
|
344
|
-
*/
|
|
309
|
+
/** The live sessions this service started, by client id, for {@link getSession}. */
|
|
345
310
|
protected readonly sessions = new Map<string, ClientSession<TAst, TDiagnostic, TTransfer>>();
|
|
346
311
|
/**
|
|
347
312
|
* Drops an ended session from {@link sessions}. Subscribed by the first
|
|
@@ -391,7 +356,6 @@ export class DefaultModelService<
|
|
|
391
356
|
) {
|
|
392
357
|
this.tracer = services.Tracer.for(options.logName ?? 'ModelService').trace('instantiated');
|
|
393
358
|
this.uriPolicy = services.workspace.DocumentUriPolicy;
|
|
394
|
-
this.slowUpdateWarn = options.slowUpdateWarnMs !== undefined ? ObservableValue.from(options.slowUpdateWarnMs) : undefined;
|
|
395
359
|
this.serializeBuilds = ObservableValue.from(options.serializeBuilds ?? true);
|
|
396
360
|
// Optional-chain so a harness that binds no WorkspaceManager awaits
|
|
397
361
|
// `undefined` and resolves immediately; production hosts always have it
|
|
@@ -533,6 +497,12 @@ export class DefaultModelService<
|
|
|
533
497
|
* {@link ModelServiceOptions.serializeBuilds} — see there for the
|
|
534
498
|
* non-reentrancy hazard that is the reason the opt-out exists.
|
|
535
499
|
*
|
|
500
|
+
* An override calls the base before it awaits. Under an LSP connection the
|
|
501
|
+
* store's change event has already asked the update handler to build a
|
|
502
|
+
* session's write; an override whose await outlasts that build makes the
|
|
503
|
+
* base's request start a build of its own, so the write is built and
|
|
504
|
+
* delivered twice.
|
|
505
|
+
*
|
|
536
506
|
* Returns an empty `{ root, diagnostics }` envelope when the document
|
|
537
507
|
* cannot be loaded; adopters that want to throw override this method
|
|
538
508
|
* on their subclass.
|
|
@@ -696,189 +666,6 @@ export class DefaultModelService<
|
|
|
696
666
|
return this.rebuild(uri, target, cancelToken);
|
|
697
667
|
}
|
|
698
668
|
|
|
699
|
-
/**
|
|
700
|
-
* Apply an update for `uri`. The structured-or-textual `model` payload
|
|
701
|
-
* is serialised (via {@link serialize} after {@link rewriteModel} when
|
|
702
|
-
* structured), pushed into the multi-client text-document store with
|
|
703
|
-
* a fresh version, drives a build to the target phase, and returns
|
|
704
|
-
* the post-build AST snapshot.
|
|
705
|
-
*
|
|
706
|
-
* **Read-latest supersession**: concurrent callers on the same URI
|
|
707
|
-
* all see the same post-build state once `waitUntil` resolves; none
|
|
708
|
-
* deadlock waiting for a specific version's settled event. The
|
|
709
|
-
* framework emits a post-resolution `debug` log line distinguishing
|
|
710
|
-
* "vN ready" from "vN ready at vM (superseded)" so callers can
|
|
711
|
-
* observe when their write was overtaken by a newer one before
|
|
712
|
-
* settling — purely observability, doesn't change resolution
|
|
713
|
-
* semantics. Adopters wanting version-matched resolution (resolve
|
|
714
|
-
* with vN's specific settled snapshot, intermediate-phase
|
|
715
|
-
* observability, slow-warn / hard-timeout behaviour) override this
|
|
716
|
-
* method.
|
|
717
|
-
*
|
|
718
|
-
* **Upsert, except for a session.** Under a plain client id the write opens
|
|
719
|
-
* the document for that client first, creating it from the payload when no
|
|
720
|
-
* file exists. Under the id of a live session from {@link createSession} it
|
|
721
|
-
* opens nothing, and fails with `DocumentNotOpenError` unless the session
|
|
722
|
-
* has the URI open at the moment the text is applied.
|
|
723
|
-
*/
|
|
724
|
-
async update(args: TransferUpdateArgs<TTransfer>, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>> {
|
|
725
|
-
// Read before the first await, while the call is still the caller's: a
|
|
726
|
-
// session that ends during the serialise would otherwise read as a plain
|
|
727
|
-
// client id here, and the write would open the document for an id no
|
|
728
|
-
// session owns any more and land there.
|
|
729
|
-
const bySession = this.sessions.has(args.clientId);
|
|
730
|
-
const stopwatch = this.slowUpdateWarn !== undefined ? this.services.Clock.stopwatch() : undefined;
|
|
731
|
-
// Per-stage self-time breakdown of the update/reconcile chain (serialise →
|
|
732
|
-
// open → apply → rebuild), opt-in at debug — the default path skips the
|
|
733
|
-
// session and `run` calls the stage directly. `update` is a per-operation
|
|
734
|
-
// method (one user save / diagram edit), not a per-node hot loop, so the
|
|
735
|
-
// stage closures `run` allocates on the non-debug path are negligible.
|
|
736
|
-
// Canonicalize the write URI once at the door and thread the resulting
|
|
737
|
-
// CanonicalUri through the chain. The text store keys documents by their
|
|
738
|
-
// canonical identity, so any spelling of a file — a canonical
|
|
739
|
-
// (server-identity) URI from a GLSP cross-document save derived from
|
|
740
|
-
// `findDocument(node).uri`, or the symlink path an editor opened — collapses
|
|
741
|
-
// to the one registration; there is no second registration to fork. The
|
|
742
|
-
// build step reuses the canonical wait core (`rebuildCanonical`) so the
|
|
743
|
-
// identity is not re-resolved downstream.
|
|
744
|
-
const uri = this.uriPolicy.canonicalUri(args.uri);
|
|
745
|
-
const session = Logger.isLevelEnabled('debug') ? this.tracer.profile(`model-update ${uri}`) : undefined;
|
|
746
|
-
const run = async <T>(stage: string, fn: () => MaybePromise<T>): Promise<T> => (session ? session.scope(stage, fn) : fn());
|
|
747
|
-
// Open WITH the new text so a cold URI (no open editor, no file on disk) is
|
|
748
|
-
// created from the payload rather than read from the filesystem — `update`
|
|
749
|
-
// is an upsert. For an already-open document `open` only attaches the
|
|
750
|
-
// client (the text is ignored on that branch), and the actual update is
|
|
751
|
-
// `AstDocumentManager.update`'s. `version` is intentionally NOT
|
|
752
|
-
// forwarded to `open`, so a cold create stays at its initial version
|
|
753
|
-
// rather than adopting a number the caller chose.
|
|
754
|
-
//
|
|
755
|
-
// The gate reads the version and decides BEFORE the open, and both halves
|
|
756
|
-
// of that matter.
|
|
757
|
-
//
|
|
758
|
-
// Reading first, because for a document no client holds open the open
|
|
759
|
-
// assigns the shared version from the INCOMING text, so a version read
|
|
760
|
-
// afterwards has already absorbed the caller's own write. Gating on it
|
|
761
|
-
// rejects every modifying write to a closed document, comparing the
|
|
762
|
-
// caller's `basedOn` against a number the caller itself produced — and a
|
|
763
|
-
// serialised round-trip that is not byte-identical to the stored text is
|
|
764
|
-
// enough to trigger that. Reading first keeps both cases the gate exists
|
|
765
|
-
// for: an unknown URI answers 0, so a based-on-version update of a
|
|
766
|
-
// not-yet-existing document still trips it, and a genuine conflict still
|
|
767
|
-
// trips it, another writer having advanced the sequence past the version
|
|
768
|
-
// the caller read.
|
|
769
|
-
//
|
|
770
|
-
// Deciding first, because the open is a mutation and `update` is an
|
|
771
|
-
// upsert: for a cold URI it installs the payload in the shared text store
|
|
772
|
-
// and records a hold for the writing client. Throwing after it hands the
|
|
773
|
-
// caller its rejection while the server keeps the refused text — and the
|
|
774
|
-
// hold outlives the call, because a client whose write failed has no
|
|
775
|
-
// reason to close a document it never asked to open. Serialisation is
|
|
776
|
-
// held back too; it is the adopter's code and need not be side-effect
|
|
777
|
-
// free.
|
|
778
|
-
const textDocuments = this.services.workspace.TextDocuments;
|
|
779
|
-
this.checkBasedOn(uri, args.basedOn, textDocuments.version(uri));
|
|
780
|
-
// Whether this call's own open is the one that creates the document, read
|
|
781
|
-
// in the same step as the check above so no other open can land between.
|
|
782
|
-
const opensCold = !bySession && textDocuments.get(uri) === undefined;
|
|
783
|
-
const text = await run('serialize', () => this.modelToText(uri, args.model, cancelToken));
|
|
784
|
-
if (!bySession) {
|
|
785
|
-
await run('open', () => this.open({ uri, clientId: args.clientId, text }));
|
|
786
|
-
}
|
|
787
|
-
const appliedVersion = await run('apply', () => {
|
|
788
|
-
// A session's write never opens its document. The check sits in the
|
|
789
|
-
// same synchronous step as the apply: made before the serialize await
|
|
790
|
-
// instead, it passes for a write whose session closes the document
|
|
791
|
-
// during that await, and the write then lands on a document the
|
|
792
|
-
// session no longer has open.
|
|
793
|
-
if (bySession && !textDocuments.isOpenInClient(uri, args.clientId)) {
|
|
794
|
-
throw new DocumentNotOpenError(uri, args.clientId);
|
|
795
|
-
}
|
|
796
|
-
// The gate again, in the same step as the apply: two writes based on
|
|
797
|
-
// one version both pass the check at the door while they serialise,
|
|
798
|
-
// and without this both apply. The version a cold open assigned from
|
|
799
|
-
// this write's own text is not a conflict; the store holding exactly
|
|
800
|
-
// that text is how it shows.
|
|
801
|
-
if (!(opensCold && textDocuments.get(uri)?.getText() === text)) {
|
|
802
|
-
this.checkBasedOn(uri, args.basedOn, textDocuments.version(uri));
|
|
803
|
-
}
|
|
804
|
-
return this.services.workspace.AstDocumentManager.update(uri, text, args.clientId);
|
|
805
|
-
});
|
|
806
|
-
// Dispatch through the public `rebuild` (which re-canonicalizes the already-
|
|
807
|
-
// canonical `uri` once, idempotently) rather than `rebuildCanonical`, so an
|
|
808
|
-
// adopter `rebuild` override stays in the update path. The redundant call is
|
|
809
|
-
// a single kernel-cached `realpath`; correctness of the override contract
|
|
810
|
-
// wins over shaving it.
|
|
811
|
-
const doc = await run('rebuild', () => this.rebuild(uri, undefined, cancelToken));
|
|
812
|
-
const finalVersion = this.services.workspace.TextDocuments.version(uri);
|
|
813
|
-
if (finalVersion > appliedVersion) {
|
|
814
|
-
this.tracer.debug(`Update to v${appliedVersion} ready at v${finalVersion} (superseded)`);
|
|
815
|
-
} else {
|
|
816
|
-
this.tracer.debug(`Update to v${appliedVersion} ready`);
|
|
817
|
-
}
|
|
818
|
-
if (this.slowUpdateWarn !== undefined && stopwatch) {
|
|
819
|
-
const elapsed = Math.round(stopwatch.elapsedMs);
|
|
820
|
-
const threshold = this.slowUpdateWarn.value;
|
|
821
|
-
if (elapsed >= threshold) {
|
|
822
|
-
this.tracer.withUri(uri).warn(`Slow update: ${elapsed}ms ≥ ${threshold}ms (v${appliedVersion}, client=${args.clientId})`);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
// One line per stage (serialise / open / apply / rebuild) + unaccounted — only when profiling.
|
|
826
|
-
session?.report('debug');
|
|
827
|
-
return doc;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
/**
|
|
831
|
-
* Throw {@link ConflictError} when `basedOn` names a version other than
|
|
832
|
-
* `currentVersion`. {@link update} calls it at the door and again in the
|
|
833
|
-
* synchronous step that applies the text, and has to call it synchronously
|
|
834
|
-
* there: a check separated from the apply by an await lets two writes based
|
|
835
|
-
* on one version both apply.
|
|
836
|
-
*/
|
|
837
|
-
protected checkBasedOn(uri: string, basedOn: BasedOn, currentVersion: number): void {
|
|
838
|
-
if (isSnapshotVersion(basedOn) && currentVersion !== basedOn) {
|
|
839
|
-
// Distinct from the post-build "superseded" debug line in `update`: this
|
|
840
|
-
// is a based-on-stale rejection (the write never applies), not two
|
|
841
|
-
// writes racing.
|
|
842
|
-
this.tracer.debug(`Conflict on ${uri}: based-on v${basedOn} stale, server at v${currentVersion}`);
|
|
843
|
-
throw new ConflictError(uri, basedOn, currentVersion);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* Persist `uri` to disk. Same content-change + settled-phase flow as
|
|
849
|
-
* {@link update}, then writes via the
|
|
850
|
-
* `WritableFileSystemProvider` and notifies the multi-client
|
|
851
|
-
* text-document store of the save (so any open LSP-side editor sees
|
|
852
|
-
* the `onDidSave` event regardless of who originated the persist).
|
|
853
|
-
*
|
|
854
|
-
* Returns the post-save AST snapshot.
|
|
855
|
-
*
|
|
856
|
-
* Under the id of a live session the save also fails with
|
|
857
|
-
* `DocumentNotOpenError` when the session closes the URI while the text is
|
|
858
|
-
* being built, and writes nothing. Once the manager has taken the text, the
|
|
859
|
-
* write completes whatever the session does next.
|
|
860
|
-
*/
|
|
861
|
-
async save(args: TransferSaveArgs<TTransfer>, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>> {
|
|
862
|
-
// Read before the first await, as in `update`: a session that ends during
|
|
863
|
-
// the rebuild would otherwise read as a plain client id below.
|
|
864
|
-
const bySession = this.sessions.has(args.clientId);
|
|
865
|
-
// Dispatch through `update` (not its internals) so an adopter `update`
|
|
866
|
-
// override — version-matched resolution, etc. — applies to saves too.
|
|
867
|
-
const doc = await this.update(args, cancelToken);
|
|
868
|
-
// Persist under the same canonical identity `update` operated on. Writing
|
|
869
|
-
// the canonical (real) path follows any symlink to the same file, and the
|
|
870
|
-
// `onDidSave` keys the one canonical registration.
|
|
871
|
-
const uri = this.uriPolicy.canonicalUri(args.uri);
|
|
872
|
-
// In the same synchronous step as the manager taking the text: checked
|
|
873
|
-
// any earlier, a session that closes the URI during the rebuild still has
|
|
874
|
-
// the shared text, other clients' edits included, written in its name.
|
|
875
|
-
if (bySession && !this.services.workspace.TextDocuments.isOpenInClient(uri, args.clientId)) {
|
|
876
|
-
throw new DocumentNotOpenError(uri, args.clientId);
|
|
877
|
-
}
|
|
878
|
-
await this.services.workspace.AstDocumentManager.save(uri, args.clientId);
|
|
879
|
-
return doc;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
669
|
/**
|
|
883
670
|
* Wait for the document at `uri` to reach the integrity-settled landmark and
|
|
884
671
|
* drain any in-flight write-path applyEdit sync chain (see {@link syncChains})
|
|
@@ -914,38 +701,6 @@ export class DefaultModelService<
|
|
|
914
701
|
}
|
|
915
702
|
}
|
|
916
703
|
|
|
917
|
-
/**
|
|
918
|
-
* Open the document at `args.uri` on behalf of `args.clientId`.
|
|
919
|
-
* Multi-client: each (uri, clientId) pair is tracked as one
|
|
920
|
-
* registration; the underlying document stays open until the last
|
|
921
|
-
* client closes it. If `args.text` is omitted the document content
|
|
922
|
-
* is read from the `FileSystemProvider`.
|
|
923
|
-
*
|
|
924
|
-
* Returns a {@link Disposable} that closes the registration when
|
|
925
|
-
* disposed — useful for `using` blocks and shutdown cleanup.
|
|
926
|
-
*
|
|
927
|
-
* Delegates to the framework-bound
|
|
928
|
-
* `services.workspace.AstDocumentManager`. Adopter subclasses with
|
|
929
|
-
* extra open-time behaviour (logging, sync-chain bootstrapping)
|
|
930
|
-
* override on their `ModelService` subclass and call `super.open`.
|
|
931
|
-
*
|
|
932
|
-
* The open path does not thread cancellation: `AstDocumentManager.open`
|
|
933
|
-
* and the filesystem read behind it take no token, so a cancelled caller
|
|
934
|
-
* still completes the open.
|
|
935
|
-
*/
|
|
936
|
-
async open(args: OpenModelArgs): Promise<Disposable> {
|
|
937
|
-
return this.services.workspace.AstDocumentManager.open(args);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* Close the document at `args.uri` for `args.clientId`. Counterpart
|
|
942
|
-
* to {@link open}; the underlying document stays open until every
|
|
943
|
-
* registered client has closed.
|
|
944
|
-
*/
|
|
945
|
-
async close(args: CloseModelArgs): Promise<void> {
|
|
946
|
-
return this.services.workspace.AstDocumentManager.close(args);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
704
|
/**
|
|
950
705
|
* True when the document at `uri` is currently open for at least one
|
|
951
706
|
* client. Pure read; no side effects.
|
|
@@ -1020,7 +775,15 @@ export class DefaultModelService<
|
|
|
1020
775
|
});
|
|
1021
776
|
let session: ClientSession<TAst, TDiagnostic, TTransfer, TOpenOptions>;
|
|
1022
777
|
try {
|
|
1023
|
-
session
|
|
778
|
+
// Unchecked: the session keeps whatever options its `open` is handed,
|
|
779
|
+
// and the factory types it for every grammar, so the narrowed type
|
|
780
|
+
// holds only for the caller that started it.
|
|
781
|
+
session = this.services.model.ClientSessionFactory.create(id, sessionLabel) as ClientSession<
|
|
782
|
+
TAst,
|
|
783
|
+
TDiagnostic,
|
|
784
|
+
TTransfer,
|
|
785
|
+
TOpenOptions
|
|
786
|
+
>;
|
|
1024
787
|
} catch (err: unknown) {
|
|
1025
788
|
textDocuments.closeSession(id);
|
|
1026
789
|
throw err;
|
|
@@ -1034,18 +797,35 @@ export class DefaultModelService<
|
|
|
1034
797
|
}
|
|
1035
798
|
|
|
1036
799
|
/**
|
|
1037
|
-
*
|
|
1038
|
-
* registered. The override point for a custom session class; narrow the
|
|
1039
|
-
* return type to it.
|
|
800
|
+
* Convert a structured-or-textual `model` payload to its textual form.
|
|
1040
801
|
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
802
|
+
* Textual payloads (LSP / pre-serialised callers) pass through untouched.
|
|
803
|
+
* Structured payloads run the per-language `UpdateRewriteService` chain
|
|
804
|
+
* (transfer-model transforms; diff-aware rewrites see the previous AST root),
|
|
805
|
+
* then serialise. The chain is the single transfer-model-transform seam — a
|
|
806
|
+
* unary normalisation is just a rewrite that ignores `previous`, as
|
|
807
|
+
* `NormalizeEmptyStringsContribution` does. The chain is empty by
|
|
808
|
+
* default, so this is a no-op for adopters that register none.
|
|
1043
809
|
*/
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
810
|
+
async modelToText(uri: string, model: TTransfer | string, cancelToken?: CancellationToken): Promise<string> {
|
|
811
|
+
if (typeof model === 'string') {
|
|
812
|
+
return model;
|
|
813
|
+
}
|
|
814
|
+
const rewritten = await this.rewriteModel(uri, model, cancelToken);
|
|
815
|
+
const target = UriUtils.toUri(uri);
|
|
816
|
+
const trivia = this.services.ServiceRegistry?.getServices(target)?.trivia?.TriviaService;
|
|
817
|
+
// Extracted BEFORE serializing, so it reads the document the write is
|
|
818
|
+
// about to replace rather than whatever a concurrent build left behind.
|
|
819
|
+
let document = this.services.workspace.LangiumDocuments.getDocument(target);
|
|
820
|
+
if (trivia !== undefined && document === undefined) {
|
|
821
|
+
const source = await this.textToTakeTriviaFrom(uri, target);
|
|
822
|
+
if (source !== undefined) {
|
|
823
|
+
document = this.services.workspace.LangiumDocumentFactory.fromString(source, target);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const extracted = trivia !== undefined && document !== undefined ? trivia.extract(document) : undefined;
|
|
827
|
+
const serialized = await this.serialize(uri, rewritten);
|
|
828
|
+
return extracted === undefined ? serialized : trivia!.apply(serialized, extracted, target);
|
|
1049
829
|
}
|
|
1050
830
|
|
|
1051
831
|
// ============================================================
|
|
@@ -1056,23 +836,14 @@ export class DefaultModelService<
|
|
|
1056
836
|
* Mirror a server-side change of `document` back to the LSP textual language
|
|
1057
837
|
* client, run per document as it reaches the post-integrity settled phase.
|
|
1058
838
|
* The single framework caller of
|
|
1059
|
-
* `HydraniumTextDocuments.applyEditToLanguageClient
|
|
1060
|
-
* `HydraniumTextDocuments.stagePendingContent`.
|
|
839
|
+
* `HydraniumTextDocuments.applyEditToLanguageClient`.
|
|
1061
840
|
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
* - **Open only in another client** (the data or GLSP head) → nothing. A
|
|
1069
|
-
* language client opening it joins the existing entry and is refreshed from
|
|
1070
|
-
* the store, and only a FIRST open reads a stage, so staging here would
|
|
1071
|
-
* leave text nothing reads.
|
|
1072
|
-
* - **Closed in every client** → {@link stageClosedDocument}, gated by
|
|
1073
|
-
* **provenance** ({@link isNonLanguageClientEdit}). With the default text
|
|
1074
|
-
* store this never stages; see {@link stageClosedDocument} for why, and
|
|
1075
|
-
* for the store it serves.
|
|
841
|
+
* Only a document open in the language client is mirrored, through
|
|
842
|
+
* {@link syncOpenDocument}, routed purely by **content**. A document open
|
|
843
|
+
* only in another client needs nothing: a language client opening it joins
|
|
844
|
+
* the existing entry and is refreshed from the store. A session writes only
|
|
845
|
+
* what it has open, so no session edit of a closed document waits here for
|
|
846
|
+
* the language client's next open.
|
|
1076
847
|
*
|
|
1077
848
|
* The decision is **re-derived from the current settled state every time** (it
|
|
1078
849
|
* is not a one-shot enrolment), which is what makes it self-healing: a doc
|
|
@@ -1092,11 +863,8 @@ export class DefaultModelService<
|
|
|
1092
863
|
// the canonical key to the recorded language-client URI(s); driving the chain
|
|
1093
864
|
// in client space here would key it under a URI `settleSave` never computes, so
|
|
1094
865
|
// the drain would miss for a divergent open path.
|
|
1095
|
-
|
|
1096
|
-
if (textDocuments.isOpenInLanguageClient(document.textDocument.uri)) {
|
|
866
|
+
if (this.services.workspace.TextDocuments.isOpenInLanguageClient(document.textDocument.uri)) {
|
|
1097
867
|
this.syncOpenDocument(document.textDocument.uri, document.textDocument.getText());
|
|
1098
|
-
} else if (!textDocuments.isOpenInAnyClient(document.textDocument.uri)) {
|
|
1099
|
-
this.stageClosedDocument(document);
|
|
1100
868
|
}
|
|
1101
869
|
}
|
|
1102
870
|
|
|
@@ -1115,53 +883,6 @@ export class DefaultModelService<
|
|
|
1115
883
|
this.queueSync(uri, text);
|
|
1116
884
|
}
|
|
1117
885
|
|
|
1118
|
-
/**
|
|
1119
|
-
* Stage the settled text of a document no client holds so the eventual
|
|
1120
|
-
* first `didOpen` sees this in-memory text instead of stale disk —
|
|
1121
|
-
* but only for a {@link isNonLanguageClientEdit genuine non-language-client edit}.
|
|
1122
|
-
* A document rebuilt by an internal build is skipped, leaving disk authoritative
|
|
1123
|
-
* on the next open.
|
|
1124
|
-
*
|
|
1125
|
-
* The default `HydraniumTextDocuments` never gets this far. Its own paths
|
|
1126
|
-
* record a version's author only while a client holds the document, and the
|
|
1127
|
-
* last close drops that history with the tracking record, so a document no
|
|
1128
|
-
* client holds reports no author and the provenance gate refuses. The stage
|
|
1129
|
-
* is reachable for a text store that keeps authorship past the last close,
|
|
1130
|
-
* or a subclass overriding {@link isNonLanguageClientEdit}.
|
|
1131
|
-
*/
|
|
1132
|
-
protected stageClosedDocument(document: LangiumDocument): void {
|
|
1133
|
-
if (this.isNonLanguageClientEdit(document)) {
|
|
1134
|
-
this.services.workspace.TextDocuments.stagePendingContent(document.textDocument.uri, document.textDocument.getText());
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
/**
|
|
1139
|
-
* Whether `document`'s settled state is a genuine edit by a client *other than
|
|
1140
|
-
* the language client* — a write a form / GLSP / integrity client actually made.
|
|
1141
|
-
* Excludes two non-edits: the **language client** itself (the LSP/Monaco text
|
|
1142
|
-
* client — it already holds its own edits, and the staging here exists to feed
|
|
1143
|
-
* it) and an **internal build** (workspace startup, a cascade relink, a
|
|
1144
|
-
* `didClose`-reload). This is the gate for {@link stageClosedDocument}: staging
|
|
1145
|
-
* an internal build would
|
|
1146
|
-
* (a) pre-stage every file on boot and (b) re-stage discarded content after
|
|
1147
|
-
* close (e.g. a disposing GLSP session's debounced submit firing after close),
|
|
1148
|
-
* which then shadows clean disk on the next open.
|
|
1149
|
-
*
|
|
1150
|
-
* The signal is "a known client other than the language client authored this
|
|
1151
|
-
* version **and** the URI was in the last build's changed set
|
|
1152
|
-
* (`isTriggeringEdit`)". A framework-internal rebuild reports no author
|
|
1153
|
-
* (`getAuthor` → `undefined`), so it fails `hasKnownAuthor` without comparing
|
|
1154
|
-
* against a sentinel. This is NOT redundant with content/registration — it
|
|
1155
|
-
* distinguishes "client edited" from "framework rebuilt", which neither the
|
|
1156
|
-
* shadow nor `isTriggeringEdit` alone can.
|
|
1157
|
-
*/
|
|
1158
|
-
protected isNonLanguageClientEdit(document: LangiumDocument): boolean {
|
|
1159
|
-
const documents = this.services.workspace.AstDocumentManager;
|
|
1160
|
-
const author = documents.getAuthor(document);
|
|
1161
|
-
const hasKnownAuthor = !!author && author !== LANGUAGE_CLIENT_ID;
|
|
1162
|
-
return hasKnownAuthor && documents.isTriggeringEdit(document.textDocument.uri);
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
886
|
/**
|
|
1166
887
|
* Enqueue a sync to the language client. The pending slot per URI holds only
|
|
1167
888
|
* the latest text: if a newer settle fires while the current RPC is in
|
|
@@ -1307,13 +1028,13 @@ export class DefaultModelService<
|
|
|
1307
1028
|
* Adopters whose serializer call shape differs (custom service
|
|
1308
1029
|
* names, generator-driven YAML pretty printers, etc.) override this
|
|
1309
1030
|
* method. Routes through `Serializer.serializeTransfer` because
|
|
1310
|
-
* `
|
|
1031
|
+
* a session's `update` / `save` always receive a
|
|
1311
1032
|
* transfer-model shape (cross-references as plain strings) from
|
|
1312
1033
|
* adopter callers — the AST-shape entry point is `serializeAst`.
|
|
1313
1034
|
*
|
|
1314
1035
|
* Returns {@link MaybePromise} so adopter `Serializer` overrides can be
|
|
1315
1036
|
* async (remote schema lookup, external canonical-value resolution); the
|
|
1316
|
-
* single caller ({@link modelToText}
|
|
1037
|
+
* single caller ({@link modelToText}) is already `async`,
|
|
1317
1038
|
* so a naive `await` covers both branches without extra ceremony.
|
|
1318
1039
|
*/
|
|
1319
1040
|
protected serialize(uri: string, root: TTransfer): MaybePromise<string> {
|
|
@@ -1326,62 +1047,12 @@ export class DefaultModelService<
|
|
|
1326
1047
|
// ============================================================
|
|
1327
1048
|
|
|
1328
1049
|
/**
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
* Structured payloads run the per-language `UpdateRewriteService` chain
|
|
1333
|
-
* (transfer-model transforms; diff-aware rewrites see the previous AST root),
|
|
1334
|
-
* then serialise. The chain is the single transfer-model-transform seam — a
|
|
1335
|
-
* unary normalisation is just a rewrite that ignores `previous`, as
|
|
1336
|
-
* `NormalizeEmptyStringsContribution` does. The chain is empty by
|
|
1337
|
-
* default, so this is a no-op for adopters that register none.
|
|
1338
|
-
*/
|
|
1339
|
-
protected async modelToText(uri: string, model: TTransfer | string, cancelToken?: CancellationToken): Promise<string> {
|
|
1340
|
-
if (typeof model === 'string') {
|
|
1341
|
-
return model;
|
|
1342
|
-
}
|
|
1343
|
-
const rewritten = await this.rewriteModel(uri, model, cancelToken);
|
|
1344
|
-
const target = UriUtils.toUri(uri);
|
|
1345
|
-
const trivia = this.services.ServiceRegistry?.getServices(target)?.trivia?.TriviaService;
|
|
1346
|
-
// Extracted BEFORE serializing, so it reads the document the write is
|
|
1347
|
-
// about to replace rather than whatever a concurrent build left behind.
|
|
1348
|
-
let document = this.services.workspace.LangiumDocuments.getDocument(target);
|
|
1349
|
-
if (trivia !== undefined && document === undefined) {
|
|
1350
|
-
const source = await this.textToTakeTriviaFrom(uri, target);
|
|
1351
|
-
if (source !== undefined) {
|
|
1352
|
-
document = this.services.workspace.LangiumDocumentFactory.fromString(source, target);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
const extracted = trivia !== undefined && document !== undefined ? trivia.extract(document) : undefined;
|
|
1356
|
-
const serialized = await this.serialize(uri, rewritten);
|
|
1357
|
-
return extracted === undefined ? serialized : trivia!.apply(serialized, extracted, target);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
/**
|
|
1361
|
-
* The text a write into an unloaded document should take its trivia from —
|
|
1362
|
-
* the open editor's if one holds it, the file's otherwise, and `undefined`
|
|
1363
|
-
* when neither can supply it.
|
|
1364
|
-
*
|
|
1365
|
-
* **A read that fails answers `undefined` rather than throwing.** `getDocument`
|
|
1366
|
-
* is a store read, so an ordinary create answers nothing here and must still
|
|
1367
|
-
* write; letting a permission error or a path that turned into a directory
|
|
1368
|
-
* escape would fail the user's write instead, having found nothing to preserve
|
|
1369
|
-
* in a file the write is about to replace anyway. Losing the comments is
|
|
1370
|
-
* recoverable, and traced; losing the write is neither.
|
|
1050
|
+
* The text a write into a document not yet built should take its trivia
|
|
1051
|
+
* from: the store's, which a write always has, since it writes only a
|
|
1052
|
+
* document its client has open.
|
|
1371
1053
|
*/
|
|
1372
|
-
protected async textToTakeTriviaFrom(uri: string,
|
|
1373
|
-
|
|
1374
|
-
if (open !== undefined) {
|
|
1375
|
-
return open;
|
|
1376
|
-
}
|
|
1377
|
-
const fileSystem = this.services.workspace.FileSystemProvider;
|
|
1378
|
-
try {
|
|
1379
|
-
return (await fileSystem.exists(target)) ? await fileSystem.readFile(target) : undefined;
|
|
1380
|
-
} catch (err: unknown) {
|
|
1381
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1382
|
-
this.tracer.withUri(uri).debug(`Could not read the file to take trivia from; writing as emitted. ${detail}`);
|
|
1383
|
-
return undefined;
|
|
1384
|
-
}
|
|
1054
|
+
protected async textToTakeTriviaFrom(uri: string, _target: URI): Promise<string | undefined> {
|
|
1055
|
+
return this.services.workspace.TextDocuments.get(uri)?.getText();
|
|
1385
1056
|
}
|
|
1386
1057
|
|
|
1387
1058
|
/**
|