/**
* Copyright (c) 2012 Cloudsmith Inc. and other contributors, as listed below.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Cloudsmith
*
*/
package org.cloudsmith.xtext.dommodel.formatter.comments;
import java.util.List;
import org.cloudsmith.xtext.dommodel.DomModelUtils;
import org.cloudsmith.xtext.dommodel.IDomNode;
import org.cloudsmith.xtext.dommodel.RegionMatch;
import org.cloudsmith.xtext.dommodel.formatter.AbstractLayout;
import org.cloudsmith.xtext.dommodel.formatter.ILayoutManager.ILayoutContext;
import org.cloudsmith.xtext.dommodel.formatter.comments.CommentProcessor.CommentFormattingOptions;
import org.cloudsmith.xtext.dommodel.formatter.comments.CommentProcessor.CommentText;
import org.cloudsmith.xtext.dommodel.formatter.comments.ICommentConfiguration.CommentType;
import org.cloudsmith.xtext.dommodel.formatter.css.StyleSet;
import org.cloudsmith.xtext.textflow.CharSequences;
import org.cloudsmith.xtext.textflow.ITextFlow;
import org.cloudsmith.xtext.textflow.TextFlow;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
/**
* <p>
* A Move-then-Fold Comment Formatting Strategy.
* </p>
* <p>
* A comment that fits at the current position (after internal formatting by the {@link CommentProcessor} is appended to the given {@link ITextFlow}
* output at its original position. If the comment does not fit, it is moved to the next line. If, on the next line, it fits at the effective indent
* (the indent of future output) the effective indent will be used, else if the comment fits at the same indent as its original line, this position is
* used. Finally, if the comment does not fit after having been moved, it is folded to fit and it is positioned at the effective indent.
* </p>
*
* <p>
* Folding only takes place if there is a space to the left of the preferred max width position.
* </p>
* <p>
* Comment formatting is performed in accordance with the configuration's {@link ICommentConfiguration}, and it is expected that the implementation is
* {@code ICommentConfiguration<{@link CommentType}>}. If not, an error message is logged, and comments are output without any formatting.
* </p>
*/
public class MoveThenFoldCommentLayout extends AbstractLayout {
@Inject
protected ICommentConfiguration<CommentType> commentConfiguration;
protected List<IDomNode> collectCommentSequence(IDomNode dom, ILayoutContext context,
ICommentContainerInformation commentConfiguration) {
List<IDomNode> result = Lists.newArrayList();
result.add(dom);
if(!commentConfiguration.isSLStyle()) {
return result;
}
final String lineSeparator = context.getLineSeparatorInformation().getLineSeparator();
final int indentSize = context.getIndentationInformation().getIndentString().length();
boolean allIsWell = false;
IDomNode next = dom;
int linePos = DomModelUtils.posOnLine(next, lineSeparator);
do {
allIsWell = false;
next = next.getNextSibling();
if(next != null && DomModelUtils.isWhitespace(next) && intersect(next, context).isContained()) {
String t = next.getText();
if(t.contains("\n")) {
allIsWell = false;
}
else {
if(Math.abs(t.length() - linePos) < indentSize) {
IDomNode next2 = next.getNextSibling();
if(next2 != null && isCompatibleComment(next2, commentConfiguration)) {
result.add(next);
result.add(next2);
next = next2;
allIsWell = true;
linePos = DomModelUtils.posOnLine(next, lineSeparator);
}
}
}
}
} while(allIsWell);
return result;
}
@Override
public boolean format(IDomNode dom, ITextFlow flow, ILayoutContext context) {
// ignores style set
return format(null, dom, flow, context);
}
@Override
public boolean format(StyleSet styleSet, IDomNode dom, ITextFlow flow, ILayoutContext context) {
RegionMatch match = intersect(dom, context);
if(match.isInside()) {
if(match.isContained() && !context.isWhitespacePreservation()) {
CommentType commentType = commentConfiguration.classify(dom);
ICommentFormatterAdvice advice = commentConfiguration.getFormatterAdvice(commentType);
if(!advice.enabled() || CommentType.Unknown == commentType) {
flow.appendText(dom.getText(), true);
return true;
}
ICommentContainerInformation commentContainer = commentConfiguration.getContainerInformation(commentType);
List<IDomNode> collected = collectCommentSequence(dom, context, commentContainer);
this.formatComment(styleSet, collected, flow, context, commentContainer, advice);
for(IDomNode n : collected)
context.markConsumed(n);
}
else
// output the part of the text that is inside the region as verbatim text
flow.appendText(match.apply().getFirst(), true);
}
return true;
}
/**
* Formats the given comment passed in {@code node} and appends the result to the given {@code output}.
*
* @param styleSet
* the effective styles for the comment as determined by the CSS
* @param node
* the comment node to format
* @param output
* the measuring text flow where output should be appended
* @param layoutContext
* the overall layout context for the formatting operation
* @param commentContainer
* description of the container of the comment text
* @param advice
*/
protected void formatComment(StyleSet styleSet, List<IDomNode> nodes, ITextFlow output,
ILayoutContext layoutContext, ICommentContainerInformation commentContext, ICommentFormatterAdvice advice) {
// How much space is left?
int maxWidth = output.getPreferredMaxWidth();
int current = output.getAppendLinePosition();
final int available = maxWidth - current;
final String lineSeparator = layoutContext.getLineSeparatorInformation().getLineSeparator();
final ICommentContainerInformation pos0Context = commentContext.create(0);
// how wide will the output be if hanging at current?
// position on line (can be -1 if there was no node model
final int pos = DomModelUtils.posOnLine(nodes.get(0), lineSeparator);
final int indentationSize = layoutContext.getIndentationInformation().getIndentString().length();
// final int pos_sameIndent = output.getLastUsedIndentation() * indentationSize;
final int pos_effectiveIndent = output.getIndentation() * indentationSize;
// number of wanted empty trailing lines in output (min and max)
final int trailing = commentContext.isSLStyle()
? 0
: 1;
final int maxTrailing = 1;
// set up extraction context (use 0 if there was no INode model)
ICommentContainerInformation in = commentContext.create(Math.max(0, pos));
CommentProcessor cpr = new CommentProcessor();
CommentText comment = cpr.separateCommentFromContainer(textOfNodes(nodes), in, lineSeparator);
// format in position 0 to measure it
TextFlow formatted = cpr.formatComment(comment, pos0Context, new CommentFormattingOptions(
advice, Integer.MAX_VALUE, trailing, maxTrailing), layoutContext);
int w = formatted.getWidth();
if(w <= available) {
// yay, it will fit as a hanging comment, reformat for this position if the position is not
// at an even indent multiple.
int use = current - pos_effectiveIndent;
if(use > 0) {
// out = commentContext.create(use);
formatted = cpr.formatComment(comment, commentContext.create(use), new CommentFormattingOptions(
advice, Integer.MAX_VALUE, trailing, maxTrailing), layoutContext);
}
output.appendText(CharSequences.trimLeft(formatted.getText())); // , true);
}
else {
// Did not fit un-wrapped
// if output ends with a break, then current is at the leftmost position already, and moving it
// is not an option. The position is also at an indent multiple, so no need to reposition the comment.
//
if(output.endsWithBreak() || output.isEmpty()) {
// re-format for effective indent
formatted = cpr.formatComment(
//
comment, //
pos0Context, //
new CommentFormattingOptions(advice, maxWidth - pos_effectiveIndent, trailing, maxTrailing),
layoutContext);
}
else {
// re-format for the available space (hanging at current position)
formatted = cpr.formatComment(//
comment, //
commentContext.create(current - pos_effectiveIndent), //
new CommentFormattingOptions(advice, available, trailing, maxTrailing), layoutContext);
// if comment formatted for hanging at current does not fit the width (because it
// has non-breakable content, or starts at an awkward position)
// then reformat and move the comment to the effective indent.
if(formatted.getWidth() > maxWidth - pos_effectiveIndent) {
// ouch, not possible to format it as hanging at current position, must move it to the next line.
output.appendBreak();
// re-format for the effective indent position space
formatted = cpr.formatComment(
//
comment, //
pos0Context, //
new CommentFormattingOptions(advice, maxWidth - pos_effectiveIndent, trailing, maxTrailing),
layoutContext);
}
}
output.appendText(CharSequences.trimLeft(formatted.getText())); // , true);
}
}
private boolean isCompatibleComment(IDomNode n, ICommentContainerInformation commentConfig) {
if(!DomModelUtils.isComment(n))
return false;
String text = n.getText();
return text != null && text.startsWith(commentConfig.getStartToken());
}
protected CharSequence textOfNodes(List<IDomNode> nodes) {
CharSequence result = CharSequences.empty();
for(IDomNode n : nodes)
result = CharSequences.concatenate(result, n.getText());
return result;
}
}