/* * Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.goide.formatter; import com.goide.GoLanguage; import com.goide.psi.*; import com.intellij.formatting.*; import com.intellij.formatting.alignment.AlignmentStrategy; import com.intellij.lang.ASTNode; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.UserDataHolderBase; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.TokenType; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.List; import static com.goide.GoParserDefinition.*; import static com.goide.GoTypes.*; public class GoFormattingModelBuilder implements FormattingModelBuilder { @NotNull private static SpacingBuilder createSpacingBuilder(@NotNull CodeStyleSettings settings) { return new SpacingBuilder(settings, GoLanguage.INSTANCE) .before(COMMA).spaceIf(false) .after(COMMA).spaceIf(true) .betweenInside(SEMICOLON, SEMICOLON, FOR_CLAUSE).spaces(1) .before(SEMICOLON).spaceIf(false) .after(SEMICOLON).spaceIf(true) .beforeInside(DOT, IMPORT_SPEC).none() .afterInside(DOT, IMPORT_SPEC).spaces(1) .around(DOT).none() .around(ASSIGN).spaces(1) .around(VAR_ASSIGN).spaces(1) .aroundInside(MUL, POINTER_TYPE).none() .before(ARGUMENT_LIST).none() .before(BUILTIN_ARGUMENT_LIST).none() .afterInside(LPAREN, ARGUMENT_LIST).none() .beforeInside(RPAREN, ARGUMENT_LIST).none() .afterInside(LPAREN, BUILTIN_ARGUMENT_LIST).none() .beforeInside(RPAREN, BUILTIN_ARGUMENT_LIST).none() .before(SIGNATURE).none() .afterInside(LPAREN, TYPE_ASSERTION_EXPR).none() .beforeInside(RPAREN, TYPE_ASSERTION_EXPR).none() .afterInside(LPAREN, PARAMETERS).none() .beforeInside(RPAREN, PARAMETERS).none() .afterInside(LPAREN, RESULT).none() .beforeInside(RPAREN, RESULT).none() .between(PARAMETERS, RESULT).spaces(1) .before(BLOCK).spaces(1) .after(FUNC).spaces(1) .after(PACKAGE).spaces(1) .after(IMPORT).spaces(1) .after(CONST).spaces(1) .after(VAR).spaces(1) .after(STRUCT).spaces(1) .after(INTERFACE).spaces(1) .after(RETURN).spaces(1) .after(GO).spaces(1) .after(DEFER).spaces(1) .after(FALLTHROUGH).spaces(1) .after(GOTO).spaces(1) .after(CONTINUE).spaces(1) .after(BREAK).spaces(1) .after(SELECT).spaces(1) .after(FOR).spaces(1) .after(IF).spaces(1) .after(ELSE).spaces(1) .before(ELSE_STATEMENT).spaces(1) .after(CASE).spaces(1) .after(RANGE).spaces(1) .after(SWITCH).spaces(1) .afterInside(SEND_CHANNEL, UNARY_EXPR).none() .aroundInside(SEND_CHANNEL, SEND_STATEMENT).spaces(1) .afterInside(CHAN, CHANNEL_TYPE).spaces(1) .afterInside(MAP, MAP_TYPE).none() .aroundInside(LBRACK, MAP_TYPE).none() .aroundInside(RBRACK, MAP_TYPE).none() .beforeInside(LITERAL_VALUE, COMPOSITE_LIT).none() .afterInside(LBRACE, LITERAL_VALUE).none() .beforeInside(LBRACE, LITERAL_VALUE).none() .afterInside(BIT_AND, UNARY_EXPR).none() .after(LINE_COMMENT).lineBreakInCode() .after(MULTILINE_COMMENT).lineBreakInCode() .between(COMM_CASE, COLON).none() .afterInside(COLON, COMM_CLAUSE).lineBreakInCode() .betweenInside(FIELD_DECLARATION, LINE_COMMENT, STRUCT_TYPE).spaces(1) .betweenInside(FIELD_DECLARATION, MULTILINE_COMMENT, STRUCT_TYPE).spaces(1) .betweenInside(LBRACK, RBRACK, ARRAY_OR_SLICE_TYPE).none() .around(ASSIGN_OP).spaces(1) .aroundInside(OPERATORS, TokenSet.create(MUL_EXPR, ADD_EXPR, OR_EXPR, CONDITIONAL_EXPR)).spaces(1) .betweenInside(LBRACE, RBRACE, BLOCK).spacing(0, 0, 0, true, 1) .afterInside(LBRACE, BLOCK).spacing(0, 0, 1, true, 1) .beforeInside(RBRACE, BLOCK).spacing(0, 0, 1, true, 1) .betweenInside(LPAREN, RPAREN, IMPORT_DECLARATION).spacing(0, 0, 0, false, 0) .afterInside(LPAREN, IMPORT_DECLARATION).spacing(0, 0, 1, false, 0) .beforeInside(RPAREN, IMPORT_DECLARATION).spacing(0, 0, 1, false, 0) .between(IMPORT_SPEC, IMPORT_SPEC).spacing(0, 0, 1, true, 1) .betweenInside(LPAREN, RPAREN, VAR_DECLARATION).spacing(0, 0, 0, false, 0) .afterInside(LPAREN, VAR_DECLARATION).spacing(0, 0, 1, false, 0) .beforeInside(RPAREN, VAR_DECLARATION).spacing(0, 0, 1, false, 0) .beforeInside(TYPE, VAR_SPEC).spaces(1) .between(VAR_SPEC, VAR_SPEC).spacing(0, 0, 1, true, 1) .betweenInside(LPAREN, RPAREN, CONST_DECLARATION).spacing(0, 0, 0, false, 0) .afterInside(LPAREN, CONST_DECLARATION).spacing(0, 0, 1, false, 0) .beforeInside(RPAREN, CONST_DECLARATION).spacing(0, 0, 1, false, 0) .beforeInside(TYPE, CONST_SPEC).spaces(1) .between(CONST_SPEC, CONST_SPEC).spacing(0, 0, 1, true, 1) .between(FIELD_DECLARATION, FIELD_DECLARATION).spacing(0, 0, 1, true, 1) .between(METHOD_SPEC, METHOD_SPEC).spacing(0, 0, 1, true, 1) ; } @NotNull @Override public FormattingModel createModel(@NotNull PsiElement element, @NotNull CodeStyleSettings settings) { Block block = new GoFormattingBlock(element.getNode(), null, Indent.getNoneIndent(), null, settings, createSpacingBuilder(settings)); return FormattingModelProvider.createFormattingModelForPsiFile(element.getContainingFile(), block, settings); } @Nullable @Override public TextRange getRangeAffectingIndent(PsiFile file, int offset, ASTNode elementAtOffset) { return null; } private static class GoFormattingBlock extends UserDataHolderBase implements ASTBlock { private static final TokenSet BLOCKS_TOKEN_SET = TokenSet.create( BLOCK, STRUCT_TYPE, INTERFACE_TYPE, SELECT_STATEMENT, EXPR_CASE_CLAUSE, TYPE_CASE_CLAUSE, LITERAL_VALUE ); private static final TokenSet BRACES_TOKEN_SET = TokenSet.create( LBRACE, RBRACE, LBRACK, RBRACK, LPAREN, RPAREN ); private static final Key<Alignment> TYPE_ALIGNMENT_INSIDE_STRUCT = Key.create("TYPE_ALIGNMENT_INSIDE_STRUCT"); @NotNull private final ASTNode myNode; @Nullable private final Alignment myAlignment; @Nullable private final Indent myIndent; @Nullable private final Wrap myWrap; @NotNull private final CodeStyleSettings mySettings; @NotNull private final SpacingBuilder mySpacingBuilder; @Nullable private List<Block> mySubBlocks; private GoFormattingBlock(@NotNull ASTNode node, @Nullable Alignment alignment, @Nullable Indent indent, @Nullable Wrap wrap, @NotNull CodeStyleSettings settings, @NotNull SpacingBuilder spacingBuilder) { myNode = node; myAlignment = alignment; myIndent = indent; myWrap = wrap; mySettings = settings; mySpacingBuilder = spacingBuilder; } @NotNull private static Indent indentIfNotBrace(@NotNull ASTNode child) { return BRACES_TOKEN_SET.contains(child.getElementType()) ? Indent.getNoneIndent() : Indent.getNormalIndent(); } private static boolean isTopLevelDeclaration(@NotNull PsiElement element) { return element instanceof GoPackageClause || element instanceof GoImportList || element instanceof GoTopLevelDeclaration && element.getParent() instanceof GoFile; } private static Spacing lineBreak() { return lineBreak(true); } private static Spacing lineBreak(boolean keepLineBreaks) { return lineBreak(0, keepLineBreaks); } private static Spacing lineBreak(int lineBreaks, boolean keepLineBreaks) { return Spacing.createSpacing(0, 0, lineBreaks + 1, keepLineBreaks, keepLineBreaks ? 1 : 0); } private static Spacing none() { return Spacing.createSpacing(0, 0, 0, false, 0); } private static Spacing one() { return Spacing.createSpacing(1, 1, 0, false, 0); } @NotNull @Override public ASTNode getNode() { return myNode; } @NotNull @Override public TextRange getTextRange() { return myNode.getTextRange(); } @Nullable @Override public Wrap getWrap() { return myWrap; } @Nullable @Override public Indent getIndent() { return myIndent; } @Nullable @Override public Alignment getAlignment() { return myAlignment; } @NotNull @Override public List<Block> getSubBlocks() { if (mySubBlocks == null) { mySubBlocks = buildSubBlocks(); } return ContainerUtil.newArrayList(mySubBlocks); } @NotNull private List<Block> buildSubBlocks() { AlignmentStrategy.AlignmentPerTypeStrategy strategy = null; boolean isStruct = getNode().getElementType() == STRUCT_TYPE; Alignment forType = null; if (isStruct) { strategy = AlignmentStrategy.createAlignmentPerTypeStrategy(ContainerUtil.list(FIELD_DECLARATION, LINE_COMMENT), STRUCT_TYPE, true); forType = Alignment.createAlignment(true); } List<Block> blocks = ContainerUtil.newArrayList(); for (ASTNode child = myNode.getFirstChildNode(); child != null; child = child.getTreeNext()) { IElementType childType = child.getElementType(); if (child.getTextRange().getLength() == 0) continue; if (childType == TokenType.WHITE_SPACE) continue; IElementType substitutor = childType == MULTILINE_COMMENT ? LINE_COMMENT : childType; Alignment alignment = strategy != null ? strategy.getAlignment(substitutor) : null; GoFormattingBlock e = buildSubBlock(child, alignment); if (isStruct) { e.putUserDataIfAbsent(TYPE_ALIGNMENT_INSIDE_STRUCT, forType); } blocks.add(e); } return Collections.unmodifiableList(blocks); } @NotNull private GoFormattingBlock buildSubBlock(@NotNull ASTNode child, @Nullable Alignment alignment) { if (child.getPsi() instanceof GoType && child.getTreeParent().getElementType() == FIELD_DECLARATION) { alignment = getUserData(TYPE_ALIGNMENT_INSIDE_STRUCT); } Indent indent = calcIndent(child); return new GoFormattingBlock(child, alignment, indent, null, mySettings, mySpacingBuilder); } @NotNull private Indent calcIndent(@NotNull ASTNode child) { IElementType parentType = myNode.getElementType(); IElementType type = child.getElementType(); if (type == SWITCH_START) return Indent.getNoneIndent(); if (parentType == BLOCK && type == SELECT_STATEMENT) return Indent.getNoneIndent(); if (parentType == SELECT_STATEMENT && type == RBRACE) return Indent.getNormalIndent(); if (parentType == ARGUMENT_LIST && type != LPAREN && type != RPAREN) return Indent.getNormalIndent(); if ((parentType == EXPR_CASE_CLAUSE || parentType == TYPE_CASE_CLAUSE) && (type == CASE || type == DEFAULT)) return Indent.getNoneIndent(); if (BLOCKS_TOKEN_SET.contains(parentType)) return indentIfNotBrace(child); if (parentType == IMPORT_DECLARATION) return indentOfMultipleDeclarationChild(type, IMPORT_SPEC); if (parentType == CONST_DECLARATION) return indentOfMultipleDeclarationChild(type, CONST_SPEC); if (parentType == VAR_DECLARATION) return indentOfMultipleDeclarationChild(type, VAR_SPEC); if (parentType == TYPE_DECLARATION) return indentOfMultipleDeclarationChild(type, TYPE_SPEC); if (parentType == COMM_CLAUSE && child.getPsi() instanceof GoStatement) return Indent.getNormalIndent(); if (child.getPsi() instanceof GoExpression) return Indent.getContinuationWithoutFirstIndent(); return Indent.getNoneIndent(); } private Indent indentOfMultipleDeclarationChild(@NotNull IElementType childType, @NotNull IElementType specType) { if (childType == specType) { return Indent.getNormalIndent(); } return COMMENTS.contains(childType) && myNode.findChildByType(specType) != null ? Indent.getNormalIndent() : Indent.getNoneIndent(); } @Override public Spacing getSpacing(@Nullable Block child1, @NotNull Block child2) { if (child1 instanceof GoFormattingBlock && child2 instanceof GoFormattingBlock) { ASTNode n1 = ((GoFormattingBlock)child1).getNode(); ASTNode n2 = ((GoFormattingBlock)child2).getNode(); PsiElement psi1 = n1.getPsi(); PsiElement psi2 = n2.getPsi(); if (n1.getElementType() == FIELD_DEFINITION && psi2 instanceof GoType) return one(); PsiElement parent = psi1.getParent(); if (parent instanceof GoStructType || parent instanceof GoInterfaceType) { boolean oneLineType = !parent.textContains('\n'); if ((n1.getElementType() == STRUCT || n1.getElementType() == INTERFACE) && n2.getElementType() == LBRACE) { return oneLineType ? none() : one(); } if (n1.getElementType() == LBRACE && n2.getElementType() == RBRACE) { return oneLineType ? none() : lineBreak(); } if (n1.getElementType() == LBRACE) { return oneLineType ? one() : lineBreak(false); } if (n2.getElementType() == RBRACE) { return oneLineType ? one() : lineBreak(false); } } if (psi1 instanceof GoStatement && psi2 instanceof GoStatement) { return lineBreak(); } if (isTopLevelDeclaration(psi2) && (isTopLevelDeclaration(psi1) || n1.getElementType() == SEMICOLON)) { // Different declarations should be separated by blank line boolean sameKind = psi1.getClass().equals(psi2.getClass()) || psi1 instanceof GoFunctionOrMethodDeclaration && psi2 instanceof GoFunctionOrMethodDeclaration; return sameKind ? lineBreak() : lineBreak(1, true); } } return mySpacingBuilder.getSpacing(this, child1, child2); } @NotNull @Override public ChildAttributes getChildAttributes(int newChildIndex) { Indent childIndent = Indent.getNoneIndent(); IElementType parentType = myNode.getElementType(); if (BLOCKS_TOKEN_SET.contains(parentType) || parentType == IMPORT_DECLARATION || parentType == CONST_DECLARATION || parentType == VAR_DECLARATION || parentType == TYPE_DECLARATION || parentType == ARGUMENT_LIST) { childIndent = Indent.getNormalIndent(); } if (parentType == EXPR_SWITCH_STATEMENT || parentType == TYPE_SWITCH_STATEMENT || parentType == SELECT_STATEMENT) { List<Block> subBlocks = getSubBlocks(); Block block = subBlocks.size() > newChildIndex ? subBlocks.get(newChildIndex - 1) : null; if (block instanceof GoFormattingBlock) { IElementType type = ((GoFormattingBlock)block).getNode().getElementType(); if (type == TYPE_CASE_CLAUSE || type == EXPR_CASE_CLAUSE) { childIndent = Indent.getNormalIndent(); } else if (type == COMM_CLAUSE) { childIndent = Indent.getNormalIndent(true); } } } return new ChildAttributes(childIndent, null); } @Override public boolean isIncomplete() { return false; } @Override public boolean isLeaf() { return myNode.getFirstChildNode() == null; } } }