GitHub Toggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto mode Back to homepage

Formatting

Langium’s formatting API allows to easily create formatters for your language. We start building a custom formatter for our language by creating a new class that inherits from AbstractFormatter.

import { AbstractFormatter, AstNode, Formatting } from 'langium';

export class CustomFormatter extends AbstractFormatter {
    protected format(node: AstNode): void {
        // This method is called for every AstNode in a document
    }
}
...
// Bind the class in your module
export const CustomModule: Module<CustomServices, PartialLangiumServices> = {
    lsp: {
        Formatter: () => new CustomFormatter()
    }
};

The entry point for the formatter is the abstract format(AstNode) method. The AbstractFormatter calls this method for every node of our model. To perform custom formatting for every type of node, we will use pattern matching. In the following example, we will take a closer look at a formatter for the domain-model language. In particular, we will see how we can format the root of our model (DomainModel) and each nested element (Entity and PackageDeclaration).

To format each node, we use the getNodeFormatter method of the AbstractFormatter. The resulting generic NodeFormatter<T extends AstNode> provides us with methods to select specific parts of a parsed AstNode such as properties or keywords.

Once we have selected the nodes of our document that we are interested in formatting, we can start applying a specific formatting. Each formatting option allows to prepend/append whitespace to each note. The Formatting namespace provides a few predefined formatting options which we can use for this:

  • newLine Adds one newline character (while preserving indentation).
  • newLines Adds a specified amount of newline characters.
  • indent Adds one level of indentation. Automatically also adds a newline character.
  • noIndent Removes all indentation.
  • oneSpace Adds one whitespace character.
  • spaces Adds a specified amount of whitespace characters.
  • noSpace Removes all spaces.
  • fit Tries to fit the existing text into one of the specified formattings.

We first start off by formatting the Domainmodel element of our DSL. It is the root node of every document and just contains a list of other elements. These elements need to be realigned to the root of the document in case they are indented. We will use the Formatting.noIndent options for that:

if (ast.isDomainmodel(node)) {
    // Create a new node formatter
    const formatter = this.getNodeFormatter(node);
    // Select a formatting region which contains all children
    const nodes = formatter.nodes(...node.elements);
    // Prepend all these nodes with no indent
    nodes.prepend(Formatting.noIndent());
}

Our other elements, namely Entity and PackageDeclaration, can be arbitrarily deeply nested, so using noIndent is out of the question for them. Instead we will use indent on everything between the { and } tokens. The formatter internally keeps track of the current indentation level:

if (ast.isEntity(node) || isPackageDeclaration(node)) {
    const formatter = this.getNodeFormatter(node);
    const bracesOpen = formatter.keyword('{');
    const bracesClose = formatter.keyword('}');
    // Add a level of indentation to each element
    // between the opening and closing braces.
    // This even includes comment nodes
    formatter.interior(bracesOpen, bracesClose).prepend(Formatting.indent());
    // Also move the newline to a closing brace
    bracesClose.prepend(Formatting.newLine());
    // Surround the name property of the element
    // With one space to each side
    formatter.property("name").surround(Formatting.oneSpace());
}

Note that most predefined Formatting methods accept additional arguments which make the resulting formatting more lenient. For example, the prepend(newLine({ allowMore: true })) formatting will not apply formatting in case the node is already preceeded by one or more newlines. It will still correctly indent the node in case the indentation is not as expected.

Full Code Sample
import { AbstractFormatter, AstNode, Formatting } from 'langium';
import * as ast from './generated/ast';

export class DomainModelFormatter extends AbstractFormatter {

    protected format(node: AstNode): void {
        if (ast.isEntity(node) || ast.isPackageDeclaration(node)) {
            const formatter = this.getNodeFormatter(node);
            const bracesOpen = formatter.keyword('{');
            const bracesClose = formatter.keyword('}');
            formatter.interior(bracesOpen, bracesClose).prepend(Formatting.indent());
            bracesClose.prepend(Formatting.newLine());
            formatter.property('name').surround(Formatting.oneSpace());
        } else if (ast.isDomainmodel(node)) {
            const formatter = this.getNodeFormatter(node);
            const nodes = formatter.nodes(...node.elements);
            nodes.prepend(Formatting.noIndent());
        }
    }
}