/** * Copyright (c) 2011,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.geppetto.pp.dsl.formatting; import java.util.Iterator; import java.util.List; import java.util.Set; import org.cloudsmith.geppetto.pp.AppendExpression; import org.cloudsmith.geppetto.pp.AssignmentExpression; import org.cloudsmith.geppetto.pp.AttributeOperations; import org.cloudsmith.geppetto.pp.Case; import org.cloudsmith.geppetto.pp.CaseExpression; import org.cloudsmith.geppetto.pp.Definition; import org.cloudsmith.geppetto.pp.DefinitionArgumentList; import org.cloudsmith.geppetto.pp.ElseExpression; import org.cloudsmith.geppetto.pp.ElseIfExpression; import org.cloudsmith.geppetto.pp.HostClassDefinition; import org.cloudsmith.geppetto.pp.IfExpression; import org.cloudsmith.geppetto.pp.JavaLambda; import org.cloudsmith.geppetto.pp.LiteralHash; import org.cloudsmith.geppetto.pp.LiteralList; import org.cloudsmith.geppetto.pp.LiteralNameOrReference; import org.cloudsmith.geppetto.pp.NodeDefinition; import org.cloudsmith.geppetto.pp.PPPackage; import org.cloudsmith.geppetto.pp.PuppetManifest; import org.cloudsmith.geppetto.pp.ResourceBody; import org.cloudsmith.geppetto.pp.ResourceExpression; import org.cloudsmith.geppetto.pp.RubyLambda; import org.cloudsmith.geppetto.pp.SelectorExpression; import org.cloudsmith.geppetto.pp.SeparatorExpression; import org.cloudsmith.geppetto.pp.SingleQuotedString; import org.cloudsmith.geppetto.pp.UnlessExpression; import org.cloudsmith.geppetto.pp.VerbatimTE; import org.cloudsmith.geppetto.pp.dsl.ppdoc.DocumentationAssociator; import org.cloudsmith.geppetto.pp.dsl.services.PPGrammarAccess; import org.cloudsmith.xtext.dommodel.DomModelUtils; import org.cloudsmith.xtext.dommodel.IDomNode; import org.cloudsmith.xtext.dommodel.RegionMatch; import org.cloudsmith.xtext.dommodel.formatter.DeclarativeSemanticFlowLayout; import org.cloudsmith.xtext.dommodel.formatter.DelegatingLayoutContext; import org.cloudsmith.xtext.dommodel.formatter.DomNodeLayoutFeeder; import org.cloudsmith.xtext.dommodel.formatter.LayoutUtils; import org.cloudsmith.xtext.dommodel.formatter.css.Alignment; import org.cloudsmith.xtext.dommodel.formatter.css.IStyleFactory; import org.cloudsmith.xtext.dommodel.formatter.css.StyleSet; import org.cloudsmith.xtext.textflow.ITextFlow; import org.cloudsmith.xtext.textflow.MeasuredTextFlow; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.AbstractElement; import org.eclipse.xtext.nodemodel.INode; import org.eclipse.xtext.util.Pair; import org.eclipse.xtext.util.Tuples; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Lists; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; /** * Semantic layouts for PP * */ @Singleton public class PPSemanticLayout extends DeclarativeSemanticFlowLayout { private static class FirstLeafWithTextAndTheRest implements Predicate<IDomNode> { private boolean firstLeafSeen = false; @Override public boolean apply(IDomNode input) { if(!DomModelUtils.isHidden(input)) firstLeafSeen = true; return firstLeafSeen; } } public enum ResourceStyle { EMPTY, SINGLEBODY_TITLE, SINGLEBODY_NO_TITLE, MULTIPLE_BODIES, COMPACTABLE; } public enum StatementStyle { /** * Statement is first in a statement list */ FIRST, /** * This is a statement. */ STATEMENT, /** * This is an unparenthesized call (an expression, not a statement) */ UNPARENTHESIZED_FUNCTION, /** * This is an argument (an expression, not a statement) */ UNPARENTHESIZED_ARG, /** * This statement is a block statement. */ BLOCK, /** * May be rendered in compact form */ COMPACTABLE, /** * Render inline */ INLINE; } @Inject private PPGrammarAccess grammarAccess; @Inject private DocumentationAssociator documentationAssociator; @Inject private DefinitionArgumentListLayout definitionListArgumentLayout; @Inject private AssignmentLayout assignmentLayout; @Inject private LiteralListLayout literaListLayout; @Inject private CaseLayout caseLayout; @Inject private SelectorLayout selectorLayout; @Inject private LiteralHashLayout literaHashLayout; @Inject private Provider<IBreakAndAlignAdvice> adviceProvider; @Inject LayoutUtils layoutUtils; /** * array of classifiers that represent {@code org.cloudsmith.geppetto.pp.dsl.formatting.PPSemanticLayout.StatementStyle.BLOCK} - used for fast * lookup (faster * that Xtext polymorph and EMF Switch) */ protected final static int[] blockClassIds = new int[] { PPPackage.CASE_EXPRESSION, PPPackage.DEFINITION, PPPackage.HOST_CLASS_DEFINITION, PPPackage.IF_EXPRESSION, PPPackage.UNLESS_EXPRESSION, PPPackage.NODE_DEFINITION, PPPackage.RESOURCE_EXPRESSION, PPPackage.SELECTOR_EXPRESSION }; private static final int ATTRIBUTE_OPERATIONS_CLUSTER_SIZE = 20; @Inject private DomNodeLayoutFeeder feeder; private static Predicate<IDomNode> untilTheEnd = Predicates.alwaysFalse(); @Inject IStyleFactory styles; protected void _after(AttributeOperations aos, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { if(aos.eContainer() instanceof ResourceBody) { flow.changeIndentation(-1); } } protected void _before(AttributeOperations aos, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { if(aos.eContainer() instanceof ResourceBody) { flow.changeIndentation(1); } } protected boolean _format(AppendExpression ae, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { return assignmentLayout._format(ae, styleSet, node, flow, context); } protected boolean _format(AssignmentExpression ae, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { return assignmentLayout._format(ae, styleSet, node, flow, context); } protected boolean _format(AttributeOperations aos, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { LayoutUtils.unifyWidthAndAlign( node, grammarAccess.getAttributeOperationAccess().getKeyAttributeNameParserRuleCall_1_0(), Alignment.left, ATTRIBUTE_OPERATIONS_CLUSTER_SIZE); return false; } protected boolean _format(Case o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { Pair<Integer, Integer> counts = internalFormatStatementList( node, grammarAccess.getCaseAccess().getStatementsExpressionListParserRuleCall_4_0()); boolean canBeCompacted = counts.getFirst() <= 1 && counts.getSecond() < 1; if(canBeCompacted && counts.getFirst() == 1) { // if the formatted statement list fits on one line, make this case eligible for same line output canBeCompacted = true; } if(canBeCompacted) node.getStyleClassifiers().add(StatementStyle.COMPACTABLE); return false; } protected boolean _format(CaseExpression o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { return caseLayout._format(o, styleSet, node, flow, context); } protected boolean _format(Definition o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { internalFormatStatementList( node, grammarAccess.getDefinitionAccess().getStatementsExpressionListParserRuleCall_4_0()); return false; } protected boolean _format(DefinitionArgumentList o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { return definitionListArgumentLayout.format(o, styleSet, node, flow, context); } protected boolean _format(ElseExpression o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { internalFormatStatementList( node, grammarAccess.getElseExpressionAccess().getStatementsExpressionListParserRuleCall_2_0()); return false; } protected boolean _format(ElseIfExpression o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { internalFormatStatementList( node, grammarAccess.getElseIfExpressionAccess().getThenStatementsExpressionListParserRuleCall_3_0()); return false; } protected boolean _format(HostClassDefinition o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { internalFormatStatementList( node, grammarAccess.getHostClassDefinitionAccess().getStatementsExpressionListParserRuleCall_5_0()); return false; } protected boolean _format(IfExpression o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { internalFormatStatementList( node, grammarAccess.getIfExpressionAccess().getThenStatementsExpressionListParserRuleCall_3_0()); return false; } protected boolean _format(JavaLambda o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { AbstractElement statements = grammarAccess.getJava8LambdaAccess().getStatementsExpressionListParserRuleCall_6_0(); AbstractElement fromElement = grammarAccess.getJava8LambdaAccess().getVerticalLineKeyword_0(); AbstractElement toElement = grammarAccess.getJava8LambdaAccess().getRightCurlyBracketKeyword_7(); return internalFormatLambda(node, flow, context, statements, fromElement, toElement); } protected boolean _format(LiteralHash o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { return literaHashLayout.format(node, flow, context); } protected boolean _format(LiteralList o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { return literaListLayout.format(node, flow, context); } protected boolean _format(NodeDefinition o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { internalFormatStatementList( node, grammarAccess.getNodeDefinitionAccess().getStatementsExpressionListParserRuleCall_5_0()); return false; } protected boolean _format(PuppetManifest manifest, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { internalFormatStatementList( node, grammarAccess.getPuppetManifestAccess().getStatementsExpressionListParserRuleCall_1_0()); return false; } protected boolean _format(ResourceExpression o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { List<Object> styles = Lists.newArrayList(); boolean compactResource = adviceProvider.get().compactResourceWhenPossible(); switch(o.getResourceData().size()) { case 0: styles.add(ResourceStyle.EMPTY); if(compactResource) styles.add(ResourceStyle.COMPACTABLE); break; case 1: styles.add(o.getResourceData().get(0).getNameExpr() != null ? ResourceStyle.SINGLEBODY_TITLE : ResourceStyle.SINGLEBODY_NO_TITLE); // if there is more than 1 attribute operation, the resource can't be compacted AttributeOperations attributes = o.getResourceData().get(0).getAttributes(); if(compactResource && (attributes == null || attributes.getAttributes().size() < 2)) styles.add(ResourceStyle.COMPACTABLE); break; default: styles.add(ResourceStyle.MULTIPLE_BODIES); break; } if(compactResource && styles.contains(ResourceStyle.COMPACTABLE)) { // must check if rendering would overflow node.getStyleClassifiers().addAll(styles); DelegatingLayoutContext dlc = new DelegatingLayoutContext(context); MeasuredTextFlow continuedFlow = new MeasuredTextFlow((MeasuredTextFlow) flow); int heightBefore = continuedFlow.getHeight(); feeder.sequence(node.getChildren(), continuedFlow, dlc, new FirstLeafWithTextAndTheRest(), untilTheEnd); if(continuedFlow.getHeight() - heightBefore > 2) { node.getStyleClassifiers().remove(ResourceStyle.COMPACTABLE); } } else { // the style is set on the container and is used in containment checks for resource bodies. node.getStyleClassifiers().addAll(styles); } return false; } protected boolean _format(RubyLambda o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { AbstractElement statements = grammarAccess.getRubyLambdaAccess().getStatementsExpressionListParserRuleCall_5_0(); AbstractElement fromElement = grammarAccess.getRubyLambdaAccess().getLAMBDATerminalRuleCall_0(); AbstractElement toElement = grammarAccess.getRubyLambdaAccess().getRightCurlyBracketKeyword_6(); return internalFormatLambda(node, flow, context, statements, fromElement, toElement); } protected boolean _format(SelectorExpression se, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { return selectorLayout._format(se, styleSet, node, flow, context); } protected boolean _format(SingleQuotedString o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { // Unless the actual string part is not marked verbatim, any literal new lines in the string will cause indentation for(IDomNode n : node.getChildren()) { if(n.getGrammarElement() == grammarAccess.getSingleQuotedStringAccess().getTextSqTextParserRuleCall_1_0()) { n.getStyles().put(styles.verbatim(true)); } } return false; } protected boolean _format(UnlessExpression o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { internalFormatStatementList( node, grammarAccess.getUnlessExpressionAccess().getThenStatementsExpressionListParserRuleCall_3_0()); return false; } protected boolean _format(VerbatimTE o, StyleSet styleSet, IDomNode node, ITextFlow flow, ILayoutContext context) { RegionMatch match = intersect(node, context); if(match.isInside()) { if(match.isContained() && !context.isWhitespacePreservation()) flow.appendText(o.getText(), true); else // output the part of the text that is inside the region as verbatim text flow.appendText(match.apply().getFirst(), true); } return true; } protected void changeInlineStyle(IDomNode node, boolean set) { List<IDomNode> nodes = node.getChildren(); Iterator<IDomNode> itor = nodes.iterator(); while(itor.hasNext()) { IDomNode n = itor.next(); IDomNode firstToken = firstSignificantNode(n); if(firstToken == null) { continue; } Set<Object> styleClassifiers = firstToken.getStyleClassifiers(); if(set) styleClassifiers.add(StatementStyle.INLINE); else styleClassifiers.remove(StatementStyle.INLINE); } } /** * Returns true if there is source text and this source contains a line break (before formatting) before a closing '}' * * @param node * @return true if the node contains a line break anywhere it its complete text. */ protected boolean containsEndLineBreak(IDomNode node) { INode n = node.getNode(); // ?s: means dotall (. matches \n) // *? means non greedy since we need to check if there is a \n before the } // optional space \s between last newline and } // return n != null && n.getText().matches("(?s:.*?)\\n\\s*\\}$"); } /** * Returns the first significant IDomNode in a Statement - this is either the first * token that represents documentation of the statement, or the first non documentation token * (for non documentable, and documentable without documentation). * * @param node * @return */ protected IDomNode firstSignificantNode(IDomNode node) { IDomNode firstToken = DomModelUtils.firstTokenWithText(node); EObject o = node.getSemanticObject(); if(o == null) return firstToken; List<INode> docNodes = documentationAssociator.getDocumentation(o); if(docNodes != null && docNodes.size() > 0) { INode firstDoc = docNodes.get(0); Iterator<IDomNode> itor = node.treeIterator(); while(itor.hasNext()) { IDomNode n = itor.next(); if(n.getNode() == firstDoc) { firstToken = n; break; } if(n == firstToken) break; // stop looking } } return firstToken; } protected boolean internalFormatLambda(IDomNode node, ITextFlow flow, ILayoutContext context, AbstractElement statements, AbstractElement fromElement, AbstractElement toElement) { boolean hasLineBreak = containsEndLineBreak(node); Pair<Integer, Integer> counts = internalFormatStatementList(node, statements); // A lambda is compactable unless it contains a linebreak, and unless it contains block expressions (that always break the line) boolean canBeCompacted = !hasLineBreak && counts.getSecond() < 1; if(canBeCompacted) { node.getStyleClassifiers().add(StatementStyle.COMPACTABLE); // Make all inline and measure if it fits (if not, revoke the inline style changeInlineStyle(node, true); // Measure only the lambda if(!layoutUtils.fitsOnSameLine(node, fromElement, toElement, flow, context)) changeInlineStyle(node, false); } else changeInlineStyle(node, false); return false; } /** * Assigns style classifiers for FIRST, STATEMENT, and BLOCK to the immediate children of the given node. * * @param node * @param grammarElement * @return count of statements */ protected Pair<Integer, Integer> internalFormatStatementList(IDomNode node, EObject grammarElement) { List<IDomNode> nodes = node.getChildren(); boolean first = true; Iterator<IDomNode> itor = nodes.iterator(); int statementCount = 0; int blockCount = 0; while(itor.hasNext()) { IDomNode n = itor.next(); if(n.getGrammarElement() == grammarElement) { IDomNode firstToken = firstSignificantNode(n); // IDomNode firstToken = DomModelUtils.firstTokenWithText(n); EObject semantic = n.getSemanticObject(); if(first) { // first in body firstToken.getStyleClassifiers().add(StatementStyle.FIRST); first = false; } if(semantic instanceof SeparatorExpression) continue; // skip marking this // mark all (except func args) as being a STATEMENT firstToken.getStyleClassifiers().add(StatementStyle.STATEMENT); statementCount++; if(isBlockStatement(semantic)) { firstToken.getStyleClassifiers().add(StatementStyle.BLOCK); blockCount++; } else if(semantic instanceof LiteralNameOrReference) { // this is an unparenthesized function call firstToken.getStyleClassifiers().add(StatementStyle.UNPARENTHESIZED_FUNCTION); // skip the optional single argument that follows if(itor.hasNext()) { n = itor.next(); } } } } return Tuples.pair(statementCount, blockCount); } /** * Returns true if the semantic object represents a block statement (one that should be * marked with {@link org.cloudsmith.geppetto.pp.dsl.formatting.PPSemanticLayout.StatementStyle.BLOCK}.) * * @param semantic * @return */ protected boolean isBlockStatement(EObject semantic) { if(semantic == null) return false; final int id = semantic.eClass().getClassifierID(); int length = blockClassIds.length; for(int i = 0; i < length; i++) if(blockClassIds[i] == id) return true; return false; } }