Skip to content

Commit 721b882

Browse files
authored
Ensure diagnostics are published after cancellation (#1566)
1 parent 34017fd commit 721b882

File tree

11 files changed

+285
-41
lines changed

11 files changed

+285
-41
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/generator-langium/templates/core/.package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"langium:watch": "langium generate --watch"
1616
},
1717
"dependencies": {
18-
"langium": "~3.1.1"
18+
"langium": "~3.1.2"
1919
},
2020
"devDependencies": {
2121
"@types/node": "^18.0.0",

packages/langium-vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
"lint": "eslint src --ext ts"
9696
},
9797
"dependencies": {
98-
"langium": "3.1.1",
98+
"langium": "3.1.2",
9999
"langium-railroad": "3.1.0",
100100
"vscode-languageserver": "~9.0.1",
101101
"ignore": "~5.2.4"

packages/langium/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Change Log of `langium`
22

3+
## v3.1.2 (Jul. 2024)
4+
5+
* Fixes a minor issue in how we determine whether a grammar internal type is a primitive or an object type ([#1563](https://github.com/eclipse-langium/langium/pull/1563)).
6+
* Introduced a new `onDocumentPhase` method for the `DocumentBuilder` interface and fixed stale references in ASTs ([#1566](https://github.com/eclipse-langium/langium/pull/1566)).
7+
* Better handle empty names in the `DocumentSymbolProvider` ([#1565](https://github.com/eclipse-langium/langium/pull/1565)).
8+
9+
## v3.1.1 (Jun. 2024)
10+
11+
* Fixed an issue in a trigger-happy validation ([#1559](https://github.com/eclipse-langium/langium/pull/1559)).
12+
313
## v3.1.0 (Jun. 2024)
414

515
### General Improvements

packages/langium/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "langium",
3-
"version": "3.1.1",
3+
"version": "3.1.2",
44
"description": "A language engineering tool for the Language Server Protocol",
55
"homepage": "https://langium.org",
66
"engines": {

packages/langium/src/grammar/langium-grammar-module.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { LangiumGrammarDefinitionProvider } from './lsp/grammar-definition.js';
2525
import { LangiumGrammarCallHierarchyProvider } from './lsp/grammar-call-hierarchy.js';
2626
import { LangiumGrammarValidationResourcesCollector } from './validation/validation-resources-collector.js';
2727
import { LangiumGrammarTypesValidator, registerTypeValidationChecks } from './validation/types-validator.js';
28-
import { interruptAndCheck } from '../utils/promise-utils.js';
2928
import { DocumentState } from '../workspace/documents.js';
3029

3130
export type LangiumGrammarAddedServices = {
@@ -104,12 +103,9 @@ export function createLangiumGrammarServices(context: DefaultSharedModuleContext
104103

105104
function addTypeCollectionPhase(sharedServices: LangiumSharedServices, grammarServices: LangiumGrammarServices) {
106105
const documentBuilder = sharedServices.workspace.DocumentBuilder;
107-
documentBuilder.onBuildPhase(DocumentState.IndexedReferences, async (documents, cancelToken) => {
108-
for (const document of documents) {
109-
await interruptAndCheck(cancelToken);
110-
const typeCollector = grammarServices.validation.ValidationResourcesCollector;
111-
const grammar = document.parseResult.value as Grammar;
112-
(document as LangiumGrammarDocument).validationResources = typeCollector.collectValidationResources(grammar);
113-
}
106+
documentBuilder.onDocumentPhase(DocumentState.IndexedReferences, async document => {
107+
const typeCollector = grammarServices.validation.ValidationResourcesCollector;
108+
const grammar = document.parseResult.value as Grammar;
109+
(document as LangiumGrammarDocument).validationResources = typeCollector.collectValidationResources(grammar);
114110
});
115111
}

packages/langium/src/lsp/language-server.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -332,17 +332,12 @@ export function addDiagnosticsHandler(connection: Connection, services: LangiumS
332332
});
333333
}
334334
});
335-
documentBuilder.onBuildPhase(DocumentState.Validated, async (documents, cancelToken) => {
336-
for (const document of documents) {
337-
if (document.diagnostics) {
338-
connection.sendDiagnostics({
339-
uri: document.uri.toString(),
340-
diagnostics: document.diagnostics
341-
});
342-
}
343-
if (cancelToken.isCancellationRequested) {
344-
return;
345-
}
335+
documentBuilder.onDocumentPhase(DocumentState.Validated, async (document) => {
336+
if (document.diagnostics) {
337+
connection.sendDiagnostics({
338+
uri: document.uri.toString(),
339+
diagnostics: document.diagnostics
340+
});
346341
}
347342
});
348343
}

packages/langium/src/references/linker.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,12 @@ export class DefaultLinker implements Linker {
114114
message: `An error occurred while resolving reference to '${ref.$refText}': ${err}`
115115
};
116116
}
117+
// Add the reference to the document's array of references
118+
// Only add if the reference has been not been resolved earlier
119+
// Otherwise we end up with duplicates
120+
// See also implementation of `buildReference`
121+
document.references.push(ref);
117122
}
118-
// Add the reference to the document's array of references
119-
document.references.push(ref);
120123
}
121124

122125
unlink(document: LangiumDocument): void {
@@ -159,6 +162,8 @@ export class DefaultLinker implements Linker {
159162
}
160163
this._ref = refData.node ?? refData.error;
161164
this._nodeDescription = refData.descr;
165+
const document = getDocument(node);
166+
document.references.push(this);
162167
}
163168
return isAstNode(this._ref) ? this._ref : undefined;
164169
},

packages/langium/src/workspace/document-builder.ts

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import type { MaybePromise } from '../utils/promise-utils.js';
1313
import type { Deferred } from '../utils/promise-utils.js';
1414
import type { ValidationOptions } from '../validation/document-validator.js';
1515
import type { IndexManager } from '../workspace/index-manager.js';
16-
import type { LangiumDocument, LangiumDocuments, LangiumDocumentFactory } from './documents.js';
16+
import type { LangiumDocument, LangiumDocuments, LangiumDocumentFactory, TextDocumentProvider } from './documents.js';
1717
import { MultiMap } from '../utils/collections.js';
18-
import { OperationCancelled, interruptAndCheck } from '../utils/promise-utils.js';
18+
import { OperationCancelled, interruptAndCheck, isOperationCancelled } from '../utils/promise-utils.js';
1919
import { stream } from '../utils/stream.js';
2020
import type { URI } from '../utils/uri-utils.js';
2121
import { ValidationCategory } from '../validation/validation-registry.js';
@@ -78,10 +78,22 @@ export interface DocumentBuilder {
7878
onUpdate(callback: DocumentUpdateListener): Disposable;
7979

8080
/**
81-
* Notify the given callback when a set of documents has been built reaching a desired target state.
81+
* Notify the given callback when a set of documents has been built reaching the specified target state.
8282
*/
8383
onBuildPhase(targetState: DocumentState, callback: DocumentBuildListener): Disposable;
8484

85+
/**
86+
* Notify the specified callback when a document has been built reaching the specified target state.
87+
* Unlike {@link onBuildPhase} the listener is called for every single document.
88+
*
89+
* There are two main advantages compared to {@link onBuildPhase}:
90+
* 1. If the build is cancelled, {@link onDocumentPhase} will still fire for documents that have reached a specific state.
91+
* Meanwhile, {@link onBuildPhase} won't fire for that state.
92+
* 2. The {@link DocumentBuilder} ensures that all {@link DocumentPhaseListener} instances are called for a built document.
93+
* Even if the build is cancelled before those listeners were called.
94+
*/
95+
onDocumentPhase(targetState: DocumentState, callback: DocumentPhaseListener): Disposable;
96+
8597
/**
8698
* Wait until the workspace has reached the specified state for all documents.
8799
*
@@ -105,6 +117,7 @@ export interface DocumentBuilder {
105117

106118
export type DocumentUpdateListener = (changed: URI[], deleted: URI[]) => void | Promise<void>
107119
export type DocumentBuildListener = (built: LangiumDocument[], cancelToken: CancellationToken) => void | Promise<void>
120+
export type DocumentPhaseListener = (built: LangiumDocument, cancelToken: CancellationToken) => void | Promise<void>
108121
export class DefaultDocumentBuilder implements DocumentBuilder {
109122

110123
updateBuildOptions: BuildOptions = {
@@ -116,17 +129,20 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
116129

117130
protected readonly langiumDocuments: LangiumDocuments;
118131
protected readonly langiumDocumentFactory: LangiumDocumentFactory;
132+
protected readonly textDocuments: TextDocumentProvider | undefined;
119133
protected readonly indexManager: IndexManager;
120134
protected readonly serviceRegistry: ServiceRegistry;
121135
protected readonly updateListeners: DocumentUpdateListener[] = [];
122136
protected readonly buildPhaseListeners = new MultiMap<DocumentState, DocumentBuildListener>();
137+
protected readonly documentPhaseListeners = new MultiMap<DocumentState, DocumentPhaseListener>();
123138
protected readonly buildState = new Map<string, DocumentBuildState>();
124139
protected readonly documentBuildWaiters = new Map<string, Deferred<void>>();
125140
protected currentState = DocumentState.Changed;
126141

127142
constructor(services: LangiumSharedCoreServices) {
128143
this.langiumDocuments = services.workspace.LangiumDocuments;
129144
this.langiumDocumentFactory = services.workspace.LangiumDocumentFactory;
145+
this.textDocuments = services.workspace.TextDocuments;
130146
this.indexManager = services.workspace.IndexManager;
131147
this.serviceRegistry = services.ServiceRegistry;
132148
}
@@ -209,22 +225,49 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
209225
// Only allow interrupting the execution after all state changes are done
210226
await interruptAndCheck(cancelToken);
211227

212-
// Collect all documents that we should rebuild
213-
const rebuildDocuments = this.langiumDocuments.all
214-
.filter(doc =>
215-
// This includes those that were reported as changed and those that we selected for relinking
216-
doc.state < DocumentState.Linked
217-
// This includes those for which a previous build has been cancelled
218-
|| !this.buildState.get(doc.uri.toString())?.completed
219-
)
220-
.toArray();
228+
// Collect and sort all documents that we should rebuild
229+
const rebuildDocuments = this.sortDocuments(
230+
this.langiumDocuments.all
231+
.filter(doc =>
232+
// This includes those that were reported as changed and those that we selected for relinking
233+
doc.state < DocumentState.Linked
234+
// This includes those for which a previous build has been cancelled
235+
|| !this.buildState.get(doc.uri.toString())?.completed
236+
)
237+
.toArray()
238+
);
221239
await this.buildDocuments(rebuildDocuments, this.updateBuildOptions, cancelToken);
222240
}
223241

224242
protected async emitUpdate(changed: URI[], deleted: URI[]): Promise<void> {
225243
await Promise.all(this.updateListeners.map(listener => listener(changed, deleted)));
226244
}
227245

246+
/**
247+
* Sort the given documents by priority. By default, documents with an open text document are prioritized.
248+
* This is useful to ensure that visible documents show their diagnostics before all other documents.
249+
*
250+
* This improves the responsiveness in large workspaces as users usually don't care about diagnostics
251+
* in files that are currently not opened in the editor.
252+
*/
253+
protected sortDocuments(documents: LangiumDocument[]): LangiumDocument[] {
254+
const hasTextDocument = new Map<LangiumDocument, boolean>();
255+
for (const doc of documents) {
256+
hasTextDocument.set(doc, Boolean(this.textDocuments?.get(doc.uri.toString())));
257+
}
258+
return documents.sort((a, b) => {
259+
const aHasDoc = hasTextDocument.get(a);
260+
const bHasDoc = hasTextDocument.get(b);
261+
if (aHasDoc && !bHasDoc) {
262+
return -1;
263+
} else if (!aHasDoc && bHasDoc) {
264+
return 1;
265+
} else {
266+
return 0;
267+
}
268+
});
269+
}
270+
228271
/**
229272
* Check whether the given document should be relinked after changes were found in the given URIs.
230273
*/
@@ -314,6 +357,7 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
314357
await interruptAndCheck(cancelToken);
315358
await callback(document);
316359
document.state = targetState;
360+
await this.notifyDocumentPhase(document, targetState, cancelToken);
317361
}
318362
await this.notifyBuildPhase(filtered, targetState, cancelToken);
319363
this.currentState = targetState;
@@ -326,6 +370,13 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
326370
});
327371
}
328372

373+
onDocumentPhase(targetState: DocumentState, callback: DocumentPhaseListener): Disposable {
374+
this.documentPhaseListeners.add(targetState, callback);
375+
return Disposable.create(() => {
376+
this.documentPhaseListeners.delete(targetState, callback);
377+
});
378+
}
379+
329380
waitUntil(state: DocumentState, cancelToken?: CancellationToken): Promise<void>;
330381
waitUntil(state: DocumentState, uri?: URI, cancelToken?: CancellationToken): Promise<URI | undefined>;
331382
waitUntil(state: DocumentState, uriOrToken?: URI | CancellationToken, cancelToken?: CancellationToken): Promise<URI | undefined | void> {
@@ -366,6 +417,21 @@ export class DefaultDocumentBuilder implements DocumentBuilder {
366417
});
367418
}
368419

420+
protected async notifyDocumentPhase(document: LangiumDocument, state: DocumentState, cancelToken: CancellationToken): Promise<void> {
421+
const listeners = this.documentPhaseListeners.get(state);
422+
for (const listener of listeners) {
423+
try {
424+
await listener(document, cancelToken);
425+
} catch (err) {
426+
// Ignore cancellation errors
427+
// We want to finish the listeners before throwing
428+
if (!isOperationCancelled(err)) {
429+
throw err;
430+
}
431+
}
432+
}
433+
}
434+
369435
protected async notifyBuildPhase(documents: LangiumDocument[], state: DocumentState, cancelToken: CancellationToken): Promise<void> {
370436
if (documents.length === 0) {
371437
// Don't notify when no document has been processed

packages/langium/src/workspace/documents.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,11 +392,13 @@ export interface LangiumDocuments {
392392
export class DefaultLangiumDocuments implements LangiumDocuments {
393393

394394
protected readonly langiumDocumentFactory: LangiumDocumentFactory;
395+
protected readonly serviceRegistry: ServiceRegistry;
395396

396397
protected readonly documentMap: Map<string, LangiumDocument> = new Map();
397398

398399
constructor(services: LangiumSharedCoreServices) {
399400
this.langiumDocumentFactory = services.workspace.LangiumDocumentFactory;
401+
this.serviceRegistry = services.ServiceRegistry;
400402
}
401403

402404
get all(): Stream<LangiumDocument> {
@@ -449,9 +451,10 @@ export class DefaultLangiumDocuments implements LangiumDocuments {
449451
const uriString = uri.toString();
450452
const langiumDoc = this.documentMap.get(uriString);
451453
if (langiumDoc) {
454+
const linker = this.serviceRegistry.getServices(uri).references.Linker;
455+
linker.unlink(langiumDoc);
452456
langiumDoc.state = DocumentState.Changed;
453457
langiumDoc.precomputedScopes = undefined;
454-
langiumDoc.references = [];
455458
langiumDoc.diagnostics = undefined;
456459
}
457460
return langiumDoc;

0 commit comments

Comments
 (0)