eclipse-langium / langium

Next-gen language engineering / DSL framework
https://langium.org/
MIT License
665 stars 61 forks source link

Formatter shows cyclic behaviour #1362

Closed insafuhrmann closed 2 weeks ago

insafuhrmann commented 5 months ago

I am implementing a code formatter for a language that has a number of constructs with code regions enclosed in keywords, in a nested fashion, for example a minimal program could be something like:

BEGIN_NAMESPACE
    myNamespace
    BEGIN_STRUCT
        myStruct
    END_STRUCT
END_NAMESPACE

I would like to have a free line before the opening BEGIN_STRUCT keyword and if there already is one or more than one, I wish to keep those lines, but when I implement it according to the documentation (at least to my understanding of it) and apply the auto format multiple times, the formatting changes back and forth, adding and removing the desired line.

Langium version: current development (3.0 candidate)

Steps To Reproduce

For example just add this to the domain-model.langium grammar:

Structure:
    'BEGIN_STRUCT' 
    name=QualifiedName
    'END_STRUCT';

NameSpace:
    'BEGIN_NAMESPACE'
    name=QualifiedName
    (elements+=AbstractElement)*
    'END_NAMESPACE';

and register the two as AbstractElement:

AbstractElement:
    PackageDeclaration | Type | Structure | NameSpace;

then add this to the format function of the DomainModelFormatter:

} else if (ast.isStructure(node)) {
            const formatter = this.getNodeFormatter(node);
            const structOpen = formatter.keyword('BEGIN_STRUCT');
            const structClose = formatter.keyword('END_STRUCT');
            formatter.interior(structOpen, structClose).prepend(Formatting.indent());
            structOpen.prepend(Formatting.newLines(2, {allowMore: true}));
            structClose.prepend(Formatting.newLine());
  } else if (ast.isNameSpace(node)) {
            const formatter = this.getNodeFormatter(node);
            const nsOpen = formatter.keyword('BEGIN_NAMESPACE');
            const nsClose = formatter.keyword('END_NAMESPACE');
            formatter.interior(nsOpen, nsClose).prepend(Formatting.indent());
            nsClose.prepend(Formatting.newLine());
  }

Then apply Format Document repeatedly to a document with the minimal program I wrote above.

The current behavior

The line before BEGIN_STRUCT alternatingly appears and is removed. More than one line is initially removed, after that same behaviour: alternate between empty line and no empty line before BEGIN_STRUCT.

The expected behavior

Depending on whether there already is a free line it should get added or simply left in the formatted code. Multiple applications of the format should not change anything after the first time. More than one free line should also be respected and not removed.