Skip to content

Conversation

@JohannesMeierSE
Copy link
Contributor

Motivation for this PR: In a Langium project, we annotated the LangiumDocument with additional properties to remember special situations in order to improve performance.

  • Re-setting/un-setting these additional properties is done in a customization of resetToState() in the DefaultDocumentBuilder.
  • During debugging, we found, that resetToState() is not called for documents which are explicitly given as changed documents to the document builder. resetToState is called only for documents which "should be relinked" due to the changed documents.

Therefore this PR aims to unify the call of resetToState in the default document builder.

Details and additional changes:

  • Added call of resetToState for all explicitly given changed documents.
  • As a consequence, LangiumDocuments.invalidateDocument became superflous, since all its statements are already part of resetToState.
    • Exception: this.indexManager.removeContent(document.uri); was missing. This didn't caused problems, since the content is replaced later with indexManager.updateContent(...). The same counts for indexManager.removeReferences and indexManager.updateReferences.
      • One could argue, that this is beneficial, since the information might be useful to identify other affected documents (shouldRelink).
      • But that strongly depends on the DSL. Additionally, it is inconsistent, since references of documents are reset at different points in time.
    • Additionally this improves separation of concerns a lot, since LangiumDocuments now focuses on storing documents (add, remove, get documents), while the document builder focuses on processing and updating document-related data. Disadvantage: This is a breaking API change!
  • Fixed an important bug: If resetToState is used to mark documents to be updated with document.state >= DocumentState.Linked and update is called later without explicitly providing the URI of this document, this document is not rebuild.
  • Introduced resetToDeleted for deleted documents, this refactoring ensures a similar code structure.
  • getDocuments was not exposed in the LangiumDocuments service.

Draft, since this PR serves for discussions and follow-up investigations.

Copy link
Contributor

@sailingKieler sailingKieler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @JohannesMeierSE! Thank you so much for taking care about this.

I have some remarks (e.g. regarding compatibility) and some improvement ideas, see below.
What do you think about them?

langiumDoc.diagnostics = undefined;
}
return langiumDoc;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change that we cannot/should not merge into a minor release of Langium 4.
But we can delegate back to the document builder's resetToState() method if a document is present. ... and keep a ToDo of consolidating with the next major release — if that will ever emerge...

}
}

resetToDeleted<T extends AstNode>(document: LangiumDocument<T>): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your attempt to stay in line with resetToState but it sounds weird to me. What about

Suggested change
resetToDeleted<T extends AstNode>(document: LangiumDocument<T>): void {
cleanUpDeleted<T extends AstNode>(document: LangiumDocument<T>): void {

expect(documentB.localSymbols).not.toBe(undefined);
expect(documentB.diagnostics).toBe(undefined);
};
await builder.update([documentA.uri], []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The builder should behave the same when triggered this way, shouldn't it?

Suggested change
await builder.update([documentA.uri], []);
await builder.update([], []);

this.langiumDocuments.addDocument(changedDocument);
}
this.resetToState(changedDocument, DocumentState.Changed);
this.buildState.delete(changedUri.toString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to move this line into resetToState (at the beginning).
Then the doc.state < DocumentState.Linked/doc.state < DocumentState.Validated part below can be dropped completely I think.

for (const doc of deletedDocs) {
deletedUris.push(doc.uri);
this.resetToDeleted(doc);
this.buildState.delete(doc.uri.toString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consequently, this line can go into the method called before.

expect(builder.actuallyBuilt).toHaveLength(2);
expect(builder.actuallyBuilt[0]).toBe('/testA.txt');
expect(builder.actuallyBuilt[1]).toBe('/testB.txt');
});
Copy link
Contributor

@sailingKieler sailingKieler Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice tests.
Could you add a sentence or two in comment on the practical use cases you had in mind for the latter two tests (if they exist)?

I would love to see one more test that cancels the build, e.g. after the scope computation phase or once one document has been linked, and resumes via another call of

await builder.update([], []);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants