Nested Conditional Formatters always create Conflict [message #1862548] |
Mon, 11 December 2023 13:01  |
Eclipse User |
|
|
|
In our language we have certain constructs that can be written both single- as well as multiline.
XText does offer the perfect solution for this in the form of as described in the presentation (https://www.slideshare.net/meysholdt/xtexts-new-formatter-api).
However, when we need to format at the end of these conditionally formatted areas, we get an exception in the case of tail-recursive elements.
I have created an almost minimal example that replicate this issue.
Let's say we have the following simple grammar of a language that only has an If statement and some terminal rules:
grammar org.xtext.example.mydsl.MyDsl with org.eclipse.xtext.common.Terminals
generate myDsl "http://www.xtext.org/example/mydsl/MyDsl"
Model:
statements+=Statement*;
Statement:
if=IfStatement | content=ShortStatement | content=LongStatement;
IfStatement:
'If' condition=Boolean ':' statement=Statement;
Boolean:
'true' | 'false';
ShortStatement:
'shortstatement';
LongStatement:
'superduperlongstatementthatdoesntfitinoneline';
I now want to format it such that the body of the if statement is either on the same line if it fits or in a new line once indented.
I wrote the following Formatter for that:
public class MyDslFormatter extends AbstractJavaFormatter {
protected void format(Model model, IFormattableDocument doc) {
for (Statement statement : model.getStatements()) {
doc.format(statement);
}
}
protected void format(Statement statement, IFormattableDocument doc) {
if (statement.getIf() != null) {
doc.format(statement.getIf());
}
}
protected void format(IfStatement ifStatement, IFormattableDocument doc) {
var regionFinder = textRegionExtensions.regionFor(ifStatement);
var booleanRegion = regionFinder.feature(MyDslPackage.Literals.IF_STATEMENT__CONDITION);
doc.prepend(regionFinder.keyword("if"), IHiddenRegionFormatter::noSpace);
doc.prepend(booleanRegion, IHiddenRegionFormatter::oneSpace);
doc.append(booleanRegion, IHiddenRegionFormatter::noSpace);
// doc.append(ifStatement, IHiddenRegionFormatter::newLine);
ISubFormatter oneline = subdoc -> {
var region = subdoc.getRegion();
subdoc = subdoc.requireFitsInLine(region.getOffset(), region.getLength(), 50);
subdoc.append(regionFinder.keyword(":"), IHiddenRegionFormatter::oneSpace);
subdoc.append(ifStatement, IHiddenRegionFormatter::newLine);
subdoc.format(ifStatement.getStatement());
};
ISubFormatter multiline = subdoc -> {
subdoc.append(regionFinder.keyword(":"), IHiddenRegionFormatter::newLine);
subdoc.append(ifStatement, IHiddenRegionFormatter::newLine);
var prevHR = textRegionExtensions.previousHiddenRegion(ifStatement.getStatement());
var nextHR = textRegionExtensions.nextHiddenRegion(ifStatement.getStatement());
subdoc.set(prevHR, nextHR, IHiddenRegionFormatter::indent);
subdoc.format(ifStatement.getStatement());
};
doc.formatConditionally(ifStatement, oneline, multiline);
}
}
This works well for singly nested if statements, but the following input: (note that after the last line there has to be some whitespace for the error to appear)
If true:
superduperlongstatementthatdoesntfitinoneline
Will create the following exception:
ERROR org.eclipse.xtext.util.ExceptionAcceptor - The region HiddenRegionReplacer must not be outside MaxLineWidthDocument.
{{{}}}: MaxLineWidthDocument at offset=0 length=57
[[[]]]: HiddenRegionReplacer at offset=57 length=2
------------------------------- document snippet -------------------------------
{{{if true:
superduperlongstatementthatdoesntfitinoneline}}}[[[
]]]
--------------------------------------------------------------------------------
org.eclipse.xtext.formatting2.internal.RegionsOutsideFrameException: The region HiddenRegionReplacer must not be outside MaxLineWidthDocument.
{{{}}}: MaxLineWidthDocument at offset=0 length=57
[[[]]]: HiddenRegionReplacer at offset=57 length=2
------------------------------- document snippet -------------------------------
{{{if true:
superduperlongstatementthatdoesntfitinoneline}}}[[[
]]]
--------------------------------------------------------------------------------
at org.eclipse.xtext.formatting2.internal.FormattableDocument.addReplacer(FormattableDocument.java:72)
at org.eclipse.xtext.formatting2.internal.MaxLineWidthDocument.addReplacer(MaxLineWidthDocument.java:34)
at org.eclipse.xtext.formatting2.internal.FormattableDocument.set(FormattableDocument.java:322)
at org.eclipse.xtext.formatting2.internal.FormattableDocument.append(FormattableDocument.java:99)
at org.xtext.example.mydsl.formatting2.MyDslFormatter.lambda$3(MyDslFormatter.java:47)
at org.eclipse.xtext.formatting2.internal.ConditionalReplacer.createReplacements(ConditionalReplacer.java:42)
at org.eclipse.xtext.formatting2.internal.FormattableDocument.createReplacements(FormattableDocument.java:147)
at org.eclipse.xtext.formatting2.internal.FormattableDocument.renderToTextReplacements(FormattableDocument.java:294)
at org.eclipse.xtext.formatting2.AbstractFormatter2.format(AbstractFormatter2.java:281)
I've already thought about including the next hidden region in the formatConditionally, but then, if there is a newline in the hidden region, it will always choose the multi-line option.
I've also already tried to simply increase the indentation inside the formatConditionally and decrease the indentation outside of it (using the api underneath the 'new' formatting api), but that becomes an issue with twice nested ifs like
If true:
If true:
superduperlongstatementthatdoesntfitinoneline
In that case, even the outer layer of the call would be at the end of a subdocument.
|
|
|
|
Re: Nested Conditional Formatters always create Conflict [message #1862614 is a reply to message #1862549] |
Thu, 14 December 2023 13:24   |
Eclipse User |
|
|
|
From what I've seen in xbase, they try to generally avoid formatConditionally, prefering to check through other means (like whether or not the area is multiline).
In the cases where they use it, it's a strict subpart of the part that the same function deals with. For instance, for closures:
[ ... | ... ]
The subdocument for the conditional formatting would only include the things between | and ] (including the whitespace immediatedly before/after these tokens, but not the tokens themselves).
That works well for most cases, but I have an issue specifically with the case where there is no token ending the area.
Weirdly enough I'm struggling to reproduce it in the minimal example right now :/
I'll update once I find a way to reproduce it otherwise it doesn't really make sense to theorise imo.
In the minimal example it seems to simply work if you change it to include the whitespace before and after the statement.
I do have a different issue when doing that, though: I would like to have each statement on its own line.
But then it will always use the multiline version, which makes sense.
he requireFitsInLine of course checks that there is no newlines and I explicitely add a newline with
doc.append(statement, IHiddenRegionFormatter::newLine);
But if I exclude it from the requireFitsInLine then I cannot do the indentation in nested statements:
{{ }}: Subdocument from requireFitsInLine
[[ ]]: Subdocument from formatConditionally
if true:{{{ if true:[[[ superduperlongstatementthatdoesntfitinoneline}}}
]]]
I do actually suspect that this might be the reason for why it didn't work in our grammar proper.
If you know a recommended way to go about this, I'd appreciate it.
Otherwise I'll continue trying to reproduce it in the minimal example today & next week and come back if/when I manage.
In any case, thanks for your help Christian :)
|
|
|
|
Powered by
FUDForum. Page generated in 0.04066 seconds