Dec 16th 2025
Mark Sujew, Dr. Miro Spönemann
Xtext, Langium, what next?
What we’ve learned from building large-scale language tooling with Langium and Xtext, and a first glimpse at a new high-performance language engineering toolkit.
Today we are excited to release Xtext 2.13.0 and I would like to give you an opinionated tour through some of the new features. There is one area where particularly much happened: Refactoring support and QuickFix support. I want to thank Jan and Dennis for implementing significant parts of it. Refactoring and QuickFix are similar: They take the DSL documents and turn them into some improved version of themselves. While refactoring focusses on restructuring, QuickFixes focus on resolving problems. The modifications applied to the DSL documents can range from very simple cases like updating an attribute’s value to complex cases such as renaming or moving multiple elements at once. To implement an operation like this, in the past, it was either necessary to obtain tokens from the node model and directly modify the text or obtain the AST and serialize the modified subtree. Unfortunately, both approaches have drawbacks:
So we spent lots of thought on how Xtext could provide hooks where all you need to do is to update your AST (the EMF model) and Xtext will take care of updating the textual DSL documents. The result is an amazingly simple service:
public interface IChangeSerializer {
interface IModification<T extends Notifier> {
void modify(T context);
}
<T extends Notifier> void addModification(T context, IModification<T> modification);
void applyModifications(IAcceptor<IEmfResourceChange> acceptor);
void setUpdateCrossReferences(boolean value);
void setUpdateRelatedFiles(boolean value);
// (...)
}
To use this service, first, you call addModification(context, modification) one or multiple times.
The parameter context is the EMF Resource or EObject that (children included) you want to modify. The parameter modification is an object or a lambda that executes the modification. When all modification have been added, it is time to call applyModifications(acceptor).
With acceptor, you provide a call-back that will be called once for every EMF Resource that needs to be changed. For XtextResources, you’ll get instances of
public interface ITextDocumentChange extends IEmfResourceChange {
URI getOldURI();
URI getNewURI();
XtextResource getResource();
List<ITextReplacement> getReplacements();
}
As promised, this gives all ITextReplacements that are needed to apply the model change back to the textual DSL documents. A lot is being considered during this process:
ISerializer .setUpdateCrossReferences(true) has been set. We built in this feature because we realized that more advanced modifications often involve many rename-refactorings.setUpdateRelatedFiles(true) has been called. Related files are the ones for which there is no explicit modification, but which contain cross references that need to be updated. Related files are determined using Xtext’s index.IFormatter2 is called for all model modifications. Thus, the resulting text changes are always nicely formatted according to how you implemented your formatter.The IChangeSerializer is part of xtext.ide and therefore available on all platforms supported by Xtext: Language Server Protocol, Eclipse IDE, etc.
In short: Resource Relocation Refactoring. The motivation behind this refactoring is the fact that many languages support syntactical elements that directly relate to the structure of the file system:
If the file structure is changed by renaming files or folders or by moving them around, the contents of one or more files needs to be updated. A perfect case for the change serializer! Also, when a file is copied it must have a different path/name compared to the original, thus making file contents changes necessary. Implementing such a hook is spectacularly simple:
class MyStrategy implements IResourceRelocationStrategy {
@Inject IResourceServiceProvider language
override applyChange(ResourceRelocationContext context) {
context.changes.filter[language.canHandle(fromURI)].forEach [ change |
context.addModification(change) [ resource |
val rootElement = resource.contents.head
if (rootElement instanceof PackageDeclaration) {
val newPackage = change.toURI.trimSegments(1).segmentsList.drop(2).join('.')
rootElement.name = newPackage
}
]
]
}
}
Even though the hook’s implementation is language-specific, it receives the URIs of all changed files, independently of which language they belong to.
This enables you to implement piggyback renaming, e.g. rename a diagram file when its model file has been renamed.
Therefore, the first thing this example does is filter out all URIs of other languages via canHandle.
Then it creates a modification lambda in which the AST is accessible and it simply updates the EAttibute name with the new package name.
The ChangeSerializer will automatically detect which other files are referencing these files and update those accorrdingly.
We also made available a new QuickFix API based on the ChangeSerializer. It has interesing capabilities:
@Fix(MyValidator.ISSUE_CODE_1)
public void fix1(final Issue issue, IssueResolutionAcceptor acceptor) {
acceptor.acceptMulti(issue, "Add Doc1", "Adds Documentation", null,
(MyElement myEObject) -> {
myEObject.setDoc("Better documentation");
}
);
}
@Fix(MyValidator.ISSUE_CODE_2)
public void fix2(final Issue issue, IssueResolutionAcceptor acceptor) {
acceptor.acceptMulti(issue, "Add Doc2", "Adds Documentation", null,
(MyElement myEObject, ICompositeModificationContext<MyElement> context) -> {
ctx.addModification(main, (obj) -> {
obj.setDoc("Better documentation");
}
);
});
}
The API works just like the old one, except that that now you’ll need to call acceptor.acceptMulti() instead of acceptor.accept().
There are two flavors of the API:
myEObject : In the lambda, you may modify any EObject from the same resource. Cross references and related files will be updated.myEObject and context . The context object gives you fine-grained control over the ChangeSerializer which is operating behind the scenes. AST-modifications are only allowed via ctx.addModification() . Since you tell ctx.addModification about the root element about the modification, elements from other resources can be modified and/or you can keep the recorded model-subtree as small as possible. Also, via the context object, you can enable/disable updating of cross references and related files.The second QuickFix enables a pattern that’s important for Multi-Fixes: First resolve everything, then modify.
For example when you’re storing EObject URIs in an issue’s user data, you’ll need to resolve them before the first modification is applied.
Otherwise, URIs may become unresolvable because the model has changed.
These two stages are automatically handled properly when you do the resolution outside of ctx.addModification() and the actual modification inside ctx.addModification’s lambda.
Rename refactoring for model elements has long been available in Eclipse Xtext. New in Xtext 2.13.0 is a flag to enable the ChangeSerializer to be the driving engine:
Workflow {
component = XtextGenerator {
...
language = StandardLanguage {
...
renameRefactoring = {
useChangeSerializer = true
...
Among the other changes in Xtext 2.13 there is:
Moritz has successfully led many software tool projects all over the world. He is one of the founders of TypeFox.