@hydranium/core 1.0.0-next.375 → 1.0.0-next.378
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/client-session-errors.d.ts +1 -1
- package/lib/documents/client-session-errors.d.ts.map +1 -1
- package/lib/documents/client-session-errors.js +1 -1
- package/lib/documents/client-session-errors.js.map +1 -1
- package/lib/langium/integrity/integrity-rule.d.ts +5 -0
- package/lib/langium/integrity/integrity-rule.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-rule.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +9 -0
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +16 -2
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts +39 -17
- package/lib/langium/model-service/client-session.d.ts.map +1 -1
- package/lib/langium/model-service/client-session.js +39 -15
- package/lib/langium/model-service/client-session.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +2 -1
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +4 -3
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/trivia/comment-preserver.d.ts.map +1 -1
- package/lib/langium/trivia/comment-preserver.js +2 -1
- package/lib/langium/trivia/comment-preserver.js.map +1 -1
- package/lib/langium/workspace/index.d.ts +1 -0
- package/lib/langium/workspace/index.d.ts.map +1 -1
- package/lib/langium/workspace/index.js +1 -0
- package/lib/langium/workspace/index.js.map +1 -1
- package/lib/langium/workspace/syntax-errors.d.ts +23 -0
- package/lib/langium/workspace/syntax-errors.d.ts.map +1 -0
- package/lib/langium/workspace/syntax-errors.js +29 -0
- package/lib/langium/workspace/syntax-errors.js.map +1 -0
- package/package.json +5 -5
- package/src/documents/client-session-errors.ts +2 -0
- package/src/langium/integrity/integrity-rule.ts +5 -0
- package/src/langium/integrity/integrity-service.ts +16 -2
- package/src/langium/model-service/client-session.ts +66 -23
- package/src/langium/model-service/model-service.ts +6 -8
- package/src/langium/trivia/comment-preserver.ts +2 -1
- package/src/langium/workspace/index.ts +1 -0
- package/src/langium/workspace/syntax-errors.ts +34 -0
|
@@ -23,11 +23,12 @@ import {
|
|
|
23
23
|
import { type AstNode, isOperationCancelled, UriUtils } from '@hydranium/langium';
|
|
24
24
|
import { type CancellationToken, type Disposable, Emitter, type Event } from 'vscode-languageserver';
|
|
25
25
|
import { type SavedAstDocument, type WrittenAstDocument } from '../../documents/ast-document-manager.js';
|
|
26
|
-
import { DocumentNotOpenError, SessionClosedError } from '../../documents/client-session-errors.js';
|
|
26
|
+
import { DocumentNotOpenError, DocumentSyntaxError, SessionClosedError } from '../../documents/client-session-errors.js';
|
|
27
27
|
import { type OpenOptions, type SessionEndCause } from '../../documents/client-session-registry.js';
|
|
28
28
|
import { type LogNameOptions } from '../diagnostics/logger.js';
|
|
29
29
|
import { type ServerSharedServices } from '../module.js';
|
|
30
30
|
import { type AstDiagnostic } from '../validation/document-validator.js';
|
|
31
|
+
import { hasSkippedText } from '../workspace/syntax-errors.js';
|
|
31
32
|
import { type ModelService } from './model-service.js';
|
|
32
33
|
|
|
33
34
|
/** What a session's `update` and `save` write into one document; the session supplies its own client id. */
|
|
@@ -41,13 +42,19 @@ export interface ClientSessionWriteArgs<TTransfer> {
|
|
|
41
42
|
* makes rather than a field it forgot.
|
|
42
43
|
*/
|
|
43
44
|
baseVersion: BaseVersion;
|
|
45
|
+
/**
|
|
46
|
+
* Set when the write restores one the session made before a reconnect. Its
|
|
47
|
+
* model was written over text that parsed, so it is not refused when the
|
|
48
|
+
* text no longer does.
|
|
49
|
+
*/
|
|
50
|
+
restore?: boolean;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
/** What a session's `persist` writes: the text the store holds for `uri`. */
|
|
47
|
-
export type ClientSessionPersistArgs = Omit<ClientSessionWriteArgs<never>, 'model'>;
|
|
54
|
+
export type ClientSessionPersistArgs = Omit<ClientSessionWriteArgs<never>, 'model' | 'restore'>;
|
|
48
55
|
|
|
49
56
|
/** What a session's `delete` removes: the document at `uri`, at the version it was based on. */
|
|
50
|
-
export type ClientSessionDeleteArgs = Omit<ClientSessionWriteArgs<never>, 'model'>;
|
|
57
|
+
export type ClientSessionDeleteArgs = Omit<ClientSessionWriteArgs<never>, 'model' | 'restore'>;
|
|
51
58
|
|
|
52
59
|
/** What a session's `updateAll` writes, all or none. */
|
|
53
60
|
export interface ClientSessionUpdateAllArgs<TTransfer> {
|
|
@@ -117,20 +124,29 @@ export interface ClientSession<
|
|
|
117
124
|
* write, so the answer holds every diagnostic its update event does: that
|
|
118
125
|
* event names this session, and a client dropping its own echo would
|
|
119
126
|
* otherwise never see them. `written` names the version the text took.
|
|
127
|
+
* A model, not text, written to a document whose last build lacks some of
|
|
128
|
+
* its text, see `hasSkippedText`, fails with `DocumentSyntaxError`, unless
|
|
129
|
+
* the write is a `restore`.
|
|
120
130
|
*/
|
|
121
131
|
update(args: ClientSessionWriteArgs<TTransfer>, cancelToken?: CancellationToken): Promise<WrittenAstDocument<TAst, TDiagnostic>>;
|
|
122
132
|
/**
|
|
123
133
|
* Write several documents this session has open, all or none: a
|
|
124
|
-
* `ConflictError` or `
|
|
125
|
-
* any text applies. Resolves to the rebuilt
|
|
126
|
-
* each validated as {@link update}'s is.
|
|
134
|
+
* `ConflictError`, `DocumentNotOpenError` or `DocumentSyntaxError` for any
|
|
135
|
+
* of them is thrown before any text applies. Resolves to the rebuilt
|
|
136
|
+
* documents, in the order given, each validated as {@link update}'s is.
|
|
127
137
|
*/
|
|
128
138
|
updateAll(
|
|
129
139
|
args: ClientSessionUpdateAllArgs<TTransfer>,
|
|
130
140
|
cancelToken?: CancellationToken
|
|
131
141
|
): Promise<WrittenAstDocument<TAst, TDiagnostic>[]>;
|
|
132
|
-
/**
|
|
133
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Write `args.model` as {@link update} does, then persist the document. A
|
|
144
|
+
* save is never a `restore`, so its model is always checked.
|
|
145
|
+
*/
|
|
146
|
+
save(
|
|
147
|
+
args: Omit<ClientSessionWriteArgs<TTransfer>, 'restore'>,
|
|
148
|
+
cancelToken?: CancellationToken
|
|
149
|
+
): Promise<SavedAstDocument<TAst, TDiagnostic>>;
|
|
134
150
|
/**
|
|
135
151
|
* Persist the text the store holds for `args.uri` as it is, with no update
|
|
136
152
|
* and no serialisation. Resolves with the version of the text written, once
|
|
@@ -277,7 +293,10 @@ export class DefaultClientSession<
|
|
|
277
293
|
return this.updateDocuments(args, cancelToken);
|
|
278
294
|
}
|
|
279
295
|
|
|
280
|
-
save(
|
|
296
|
+
save(
|
|
297
|
+
args: Omit<ClientSessionWriteArgs<TTransfer>, 'restore'>,
|
|
298
|
+
cancelToken?: CancellationToken
|
|
299
|
+
): Promise<SavedAstDocument<TAst, TDiagnostic>> {
|
|
281
300
|
this.assertLive();
|
|
282
301
|
return this.saveDocument(args, cancelToken);
|
|
283
302
|
}
|
|
@@ -353,16 +372,29 @@ export class DefaultClientSession<
|
|
|
353
372
|
}
|
|
354
373
|
}
|
|
355
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Throw `DocumentSyntaxError` when the parse of `uri`'s last build lacks
|
|
377
|
+
* some of its text, see `hasSkippedText`. A write of a model calls it beside
|
|
378
|
+
* {@link assertBaseVersion}: the model is serialized over the whole text,
|
|
379
|
+
* and one read from such a document lacks what the parser skipped.
|
|
380
|
+
*/
|
|
381
|
+
protected assertParses(uri: string): void {
|
|
382
|
+
const document = this.modelService.getDocument(uri);
|
|
383
|
+
if (document && hasSkippedText(document)) {
|
|
384
|
+
throw new DocumentSyntaxError(uri);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
356
388
|
/**
|
|
357
389
|
* Serialise `args.model`, apply it to the store under this session's id,
|
|
358
390
|
* rebuild, and resolve to the rebuilt document.
|
|
359
391
|
*
|
|
360
|
-
* The open check
|
|
361
|
-
* cannot land is refused before any
|
|
362
|
-
*
|
|
363
|
-
* serialiser's await, a close during it
|
|
364
|
-
* this session no longer has open, and
|
|
365
|
-
* both apply.
|
|
392
|
+
* The open check, the `baseVersion` gate and, for a model, the parse check
|
|
393
|
+
* run at the door, so a write that cannot land is refused before any
|
|
394
|
+
* adopter serialiser runs, and again in the synchronous step that applies
|
|
395
|
+
* the text: made only before the serialiser's await, a close during it
|
|
396
|
+
* lets the write land on a document this session no longer has open, and
|
|
397
|
+
* two writes based on one version both apply.
|
|
366
398
|
*
|
|
367
399
|
* Resolves with the latest build of the document, which may already carry
|
|
368
400
|
* a newer write; that is logged at debug, and a slow update at warn when
|
|
@@ -381,13 +413,18 @@ export class DefaultClientSession<
|
|
|
381
413
|
const profile = Logger.isLevelEnabled('debug') ? this.tracer.profile(`model-update ${uri}`) : undefined;
|
|
382
414
|
const run = async <T>(stage: string, fn: () => MaybePromise<T>): Promise<T> => (profile ? profile.scope(stage, fn) : fn());
|
|
383
415
|
const textDocuments = this.services.workspace.TextDocuments;
|
|
384
|
-
|
|
385
|
-
|
|
416
|
+
const check = (): void => {
|
|
417
|
+
this.assertOpen(uri);
|
|
418
|
+
this.assertBaseVersion(uri, args.baseVersion, textDocuments.version(uri));
|
|
419
|
+
if (typeof args.model !== 'string' && !args.restore) {
|
|
420
|
+
this.assertParses(uri);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
check();
|
|
386
424
|
service.assertCanBuild(uri);
|
|
387
425
|
const text = await run('serialize', () => service.modelToText(uri, args.model, cancelToken));
|
|
388
426
|
const appliedVersion = await run('apply', () => {
|
|
389
|
-
|
|
390
|
-
this.assertBaseVersion(uri, args.baseVersion, textDocuments.version(uri));
|
|
427
|
+
check();
|
|
391
428
|
return this.services.workspace.AstDocumentManager.update(uri, text, this.clientId);
|
|
392
429
|
});
|
|
393
430
|
// Through the public `rebuild`, so an override of it stays in the path.
|
|
@@ -414,9 +451,10 @@ export class DefaultClientSession<
|
|
|
414
451
|
|
|
415
452
|
/**
|
|
416
453
|
* Write every document of `args.updates`, all or none: every model is
|
|
417
|
-
* serialised first, then every open check
|
|
418
|
-
* text is applied in one synchronous step. Checked at
|
|
419
|
-
* a stale set is refused before any adopter serialiser
|
|
454
|
+
* serialised first, then every open check, `baseVersion` gate and parse
|
|
455
|
+
* check runs and every text is applied in one synchronous step. Checked at
|
|
456
|
+
* the door as well, so a stale set is refused before any adopter serialiser
|
|
457
|
+
* runs.
|
|
420
458
|
*
|
|
421
459
|
* The apply step relies on `AstDocumentManager.update` applying its text
|
|
422
460
|
* before its first await, as the default does: an override that awaits first
|
|
@@ -437,6 +475,9 @@ export class DefaultClientSession<
|
|
|
437
475
|
updates.forEach((update, i) => {
|
|
438
476
|
this.assertOpen(uris[i]);
|
|
439
477
|
this.assertBaseVersion(uris[i], update.baseVersion, textDocuments.version(uris[i]));
|
|
478
|
+
if (typeof update.model !== 'string' && !update.restore) {
|
|
479
|
+
this.assertParses(uris[i]);
|
|
480
|
+
}
|
|
440
481
|
service.assertCanBuild(uris[i]);
|
|
441
482
|
});
|
|
442
483
|
check();
|
|
@@ -479,12 +520,14 @@ export class DefaultClientSession<
|
|
|
479
520
|
* Fails with `DocumentNotOpenError` and writes nothing when this session
|
|
480
521
|
* closes the URI while the text is being built. Persisted at `'any'`: the
|
|
481
522
|
* update already gated on `args.baseVersion` and moved the text past it.
|
|
523
|
+
* A `restore` in `args` is dropped: a save puts the model on disk, so
|
|
524
|
+
* it is always checked.
|
|
482
525
|
*/
|
|
483
526
|
protected async saveDocument(
|
|
484
527
|
args: ClientSessionWriteArgs<TTransfer>,
|
|
485
528
|
cancelToken?: CancellationToken
|
|
486
529
|
): Promise<SavedAstDocument<TAst, TDiagnostic>> {
|
|
487
|
-
const doc = await this.updateDocument(args, cancelToken);
|
|
530
|
+
const doc = await this.updateDocument({ ...args, restore: undefined }, cancelToken);
|
|
488
531
|
const version = await this.persistDocument({ uri: args.uri, baseVersion: 'any' });
|
|
489
532
|
return { ...doc, persisted: { version } };
|
|
490
533
|
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from '@hydranium/langium';
|
|
33
33
|
import { type AstDiagnostic } from '../validation/document-validator.js';
|
|
34
34
|
import { type DocumentUriPolicy } from '../workspace/document-uri-policy.js';
|
|
35
|
+
import { hasSyntaxErrors } from '../workspace/syntax-errors.js';
|
|
35
36
|
import { ReentrantWriteLockError, isInsideWriteLock, isWriteLockScopeInstalled } from '../workspace/write-lock-scope.js';
|
|
36
37
|
import { type ApplyWorkspaceEditResult, CancellationToken, Disposable } from 'vscode-languageserver';
|
|
37
38
|
import { AstDocument } from '../../documents/ast-document-manager.js';
|
|
@@ -123,7 +124,8 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
123
124
|
* Costs a parse, an encode and a serialization per write whose text
|
|
124
125
|
* differs; remembering the text last written instead would not cover a
|
|
125
126
|
* file formatted outside the framework. Text with lexer or parser errors is
|
|
126
|
-
* replaced as usual.
|
|
127
|
+
* replaced as usual by {@link ModelService.modelToText}, though a session
|
|
128
|
+
* refuses to write a model there.
|
|
127
129
|
*
|
|
128
130
|
* Accepts a {@link MaybeObservableValue} so it can be bound to a setting.
|
|
129
131
|
*/
|
|
@@ -1106,18 +1108,14 @@ export class DefaultModelService<
|
|
|
1106
1108
|
// equal and apply the text from before the mutation. Parsed into a
|
|
1107
1109
|
// document at `target`, so the encoder's hooks see the URI the written
|
|
1108
1110
|
// side was encoded with.
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
lexerErrors,
|
|
1112
|
-
parserErrors
|
|
1113
|
-
} = this.services.workspace.LangiumDocumentFactory.fromString(parsedText, target).parseResult;
|
|
1114
|
-
if (lexerErrors.length > 0 || parserErrors.length > 0) {
|
|
1111
|
+
const parsed = this.services.workspace.LangiumDocumentFactory.fromString(parsedText, target).parseResult;
|
|
1112
|
+
if (hasSyntaxErrors(parsed)) {
|
|
1115
1113
|
return text;
|
|
1116
1114
|
}
|
|
1117
1115
|
// Through `serialize`, so an override of it applies to both sides. Not
|
|
1118
1116
|
// rewritten: a rewrite that changes the document's model then compares
|
|
1119
1117
|
// unequal and is written, rather than hidden behind the kept text.
|
|
1120
|
-
const transfer = this.services.model.TransferEncoder.toTransfer(
|
|
1118
|
+
const transfer = this.services.model.TransferEncoder.toTransfer(parsed.value, 'grammar') as TTransfer;
|
|
1121
1119
|
const current = withTrivia(await this.serialize(uri, transfer));
|
|
1122
1120
|
return current === text ? parsedText : text;
|
|
1123
1121
|
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '@hydranium/langium';
|
|
23
23
|
import { type LogNameOptions } from '../diagnostics/logger.js';
|
|
24
24
|
import { type HydraniumLanguageServices } from '../language-module.js';
|
|
25
|
+
import { hasSyntaxErrors } from '../workspace/syntax-errors.js';
|
|
25
26
|
import { type TriviaContribution, type TriviaRegistry } from './trivia-contribution.js';
|
|
26
27
|
import { type TriviaPreserver } from './trivia-preserver.js';
|
|
27
28
|
|
|
@@ -768,7 +769,7 @@ export class CommentPreserver implements TriviaPreserver<CommentTrivia> {
|
|
|
768
769
|
} catch {
|
|
769
770
|
document = undefined;
|
|
770
771
|
}
|
|
771
|
-
if (document !== undefined && (document.parseResult
|
|
772
|
+
if (document !== undefined && hasSyntaxErrors(document.parseResult)) {
|
|
772
773
|
document = undefined;
|
|
773
774
|
}
|
|
774
775
|
if (memo !== undefined) {
|
|
@@ -13,6 +13,7 @@ export * from './file-system-provider.js';
|
|
|
13
13
|
export * from './in-memory-file-system-provider.js';
|
|
14
14
|
export * from './persistent-file-system-provider.js';
|
|
15
15
|
export * from './self-save-registry.js';
|
|
16
|
+
export * from './syntax-errors.js';
|
|
16
17
|
export * from './index-manager.js';
|
|
17
18
|
export * from './initialize-workspace.js';
|
|
18
19
|
export * from './langium-documents.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { type LangiumDocument, type ParseResult } from '@hydranium/langium';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether `parseResult` has a lexer error, a parser error or an error a token
|
|
14
|
+
* builder reported. The AST of such a parse lacks what the parser skipped, so
|
|
15
|
+
* text serialized from it deletes that. A client reads the same from
|
|
16
|
+
* diagnostics with `TransferDiagnostic.hasSyntaxErrors`, which can miss some.
|
|
17
|
+
*/
|
|
18
|
+
export function hasSyntaxErrors(parseResult: ParseResult): boolean {
|
|
19
|
+
const lexing = parseResult.lexerReport?.diagnostics ?? [];
|
|
20
|
+
return (
|
|
21
|
+
parseResult.parserErrors.length > 0 ||
|
|
22
|
+
parseResult.lexerErrors.length > 0 ||
|
|
23
|
+
lexing.some(diagnostic => (diagnostic.severity ?? 'error') === 'error')
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether the parse of `document` lacks some of its text: it has syntax
|
|
29
|
+
* errors and the text is not blank. A model serialized from its AST deletes
|
|
30
|
+
* that text, so a write of one is refused. Blank text has nothing to lose.
|
|
31
|
+
*/
|
|
32
|
+
export function hasSkippedText(document: LangiumDocument): boolean {
|
|
33
|
+
return hasSyntaxErrors(document.parseResult) && document.textDocument.getText().trim() !== '';
|
|
34
|
+
}
|