/* * Copyright 2000-2014 JetBrains s.r.o. * * 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.intellij.formatting.alignment; import com.intellij.lang.ASTNode; import com.intellij.openapi.util.Couple; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.impl.source.tree.TreeUtil; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; import com.intellij.util.SmartList; import org.jetbrains.annotations.Nullable; import java.util.List; /** * This class provides helper methods to use for {@code 'align in columns'} processing. * <p/> * {@code 'Align in columns'} here means format the code like below: * <pre> * class Test { * private int iii = 1; * private double d = 2; * } * </pre> * I.e. components of two lines are aligned to each other in columns. * <p/> * This class is not singleton but it's thread-safe and provides single-point-of-usage field {@link #INSTANCE}. * <p/> * Thread-safe. * * @author Denis Zhdanov * @since May 24, 2010 3:09:52 PM */ public class AlignmentInColumnsHelper { /** * Single-point-of-usage field. */ public static final AlignmentInColumnsHelper INSTANCE = new AlignmentInColumnsHelper(); /** * Allows to answer if given node should be aligned to the previous node of the same type according to the given alignment config * assuming that given node is a variable declaration. * * @param node target node which alignment strategy is to be defined * @param config alignment config to use for processing * @param blankLinesToBeKeptOnReformat corresponding KEEP_LINE_IN_* formatting setting * @return {@code true} if given node should be aligned to the previous one; {@code false} otherwise */ @SuppressWarnings({"MethodMayBeStatic"}) public boolean useDifferentVarDeclarationAlignment(ASTNode node, AlignmentInColumnsConfig config, int blankLinesToBeKeptOnReformat) { ASTNode prev = getPreviousAdjacentNodeOfTargetType(node, config, blankLinesToBeKeptOnReformat); if (prev == null) { return true; } ASTNode curr = deriveNodeOfTargetType(node, TokenSet.create(prev.getElementType())); if (curr == null) { return true; } // The main idea is to avoid alignment like the one below: // private final int i; // double d; // I.e. we want to avoid alignment-implied long indents from the start of line. // Please note that we do allow alignment like below: // private final int i; // private double d; ASTNode prevSubNode = getSubNodeThatStartsNewLine(prev.getFirstChildNode(), config); ASTNode currSubNode = getSubNodeThatStartsNewLine(curr.getFirstChildNode(), config); while (true) { boolean prevNodeIsDefined = prevSubNode != null; boolean currNodeIsDefined = currSubNode != null; // Check if one sub-node starts from new line and another one doesn't start. if (prevNodeIsDefined ^ currNodeIsDefined) { return true; } if (prevSubNode == null) { break; } if (prevSubNode.getElementType() != currSubNode.getElementType() /*|| StringUtil.countNewLines(prevSubNode.getChars()) != StringUtil.countNewLines(currSubNode.getChars())*/) { return true; } prevSubNode = getSubNodeThatStartsNewLine(prevSubNode.getTreeNext(), config); currSubNode = getSubNodeThatStartsNewLine(currSubNode.getTreeNext(), config); } // There is a possible declaration like the one below // int i1 = 1; // int i2, i3 = 2; // Three fields are declared here - 'i1', 'i2' and 'i3'. So, the check if field 'i2' contains assignment should be // performed against 'i3'. ASTNode currentFieldToUse = curr; ASTNode nextNode = curr.getTreeNext(); for (; nextNode != null && nextNode.getTreeParent() == curr.getTreeParent(); nextNode = nextNode.getTreeNext()) { IElementType type = nextNode.getElementType(); if (config.getWhiteSpaceTokenTypes().contains(type)) { ASTNode previous = nextNode.getTreePrev(); if ((previous != null && previous.getElementType() == curr.getElementType()) || StringUtil.countNewLines(nextNode.getChars()) > 1) { break; } continue; } if (config.getCommentTokenTypes().contains(type)) { continue; } if (type == curr.getElementType()) { currentFieldToUse = nextNode; } } List<IElementType> prevTypes = findSubNodeTypes(prev, config.getDistinguishableTypes()); List<IElementType> currTypes = findSubNodeTypes(currentFieldToUse, config.getDistinguishableTypes()); return !prevTypes.equals(currTypes); } /** * Tries to find previous node adjacent to the given node that has the same * {@link AlignmentInColumnsConfig#getTargetDeclarationTypes() target type}. * * @param baseNode base node to use * @param config current processing config * @param blankLinesToBeKeptOnReformat * @return previous node to the given base node that has that same type and is adjacent to it if possible; * {@code null} otherwise */ @SuppressWarnings({"StatementWithEmptyBody"}) @Nullable private static ASTNode getPreviousAdjacentNodeOfTargetType(ASTNode baseNode, AlignmentInColumnsConfig config, final double blankLinesToBeKeptOnReformat) { ASTNode nodeOfTargetType = deriveNodeOfTargetType(baseNode, config.getTargetDeclarationTypes()); if (nodeOfTargetType == null) { return null; } final ASTNode[] prev = new ASTNode[1]; findPreviousNode(config, baseNode, new NodeProcessor() { @Override public boolean targetTypeFound(ASTNode node) { prev[0] = node; return true; } @Override public boolean whitespaceFound(ASTNode node) { return blankLinesToBeKeptOnReformat > 0 && StringUtil.countChars(node.getText(), '\n') > 1; } }); if (prev[0] == null) return null; // ensure there are no non-whitespace, non-comment elements on the top level between baseNode and the found one Couple<ASTNode> siblingParents = TreeUtil.findTopmostSiblingParents(prev[0], baseNode); if (siblingParents.first != null && siblingParents.second != null) { for (ASTNode each = siblingParents.second.getTreePrev(); each != null && each != siblingParents.first; each = each.getTreePrev()) { IElementType eachType = each.getElementType(); if (!config.getCommentTokenTypes().contains(eachType) && !config.getWhiteSpaceTokenTypes().contains(eachType)) return null; } } return deriveNodeOfTargetType(prev[0], TokenSet.create(nodeOfTargetType.getElementType())); } /** * There is a possible case that given base node doesn't have {@link AlignmentInColumnsConfig#getTargetDeclarationTypes() target type} * but its first child node or first child node of the first child node etc does. * <p/> * This method tries to derive node of the target type from the given node. * * @param baseNode base node to process * @param targetTypes target node types * @return base node or its first descendant child that has * {@link AlignmentInColumnsConfig#getTargetDeclarationTypes() target type} target type if the one if found; * {@code null} otherwise */ @Nullable private static ASTNode deriveNodeOfTargetType(ASTNode baseNode, TokenSet targetTypes) { if (targetTypes.contains(baseNode.getElementType())) { return baseNode; } for (ASTNode node = baseNode; node != null; node = node.getFirstChildNode()) { IElementType nodeType = node.getElementType(); if (targetTypes.contains(nodeType)) { return node; } } return null; } /** * Shorthand for calling {@link #findPreviousNode(AlignmentInColumnsConfig, ASTNode, NodeProcessor)} with the type of * the given node as a target type. * * @param config configuration to use * @param from start node to use * @param processor * @return true if the processor has returned true for one of the processed nodes, false otherwise */ private static boolean findPreviousNode(AlignmentInColumnsConfig config, ASTNode from, NodeProcessor processor) { return findPreviousNode(config, from, from.getElementType(), false, true, processor); } /** * Tries to find node that is direct or indirect previous node of the given node. * <p/> * E.g. there is a possible use-case: * <pre> * n1 * / \ * n21 n22 * | | * n31 n32 * </pre> * Let's assume that target node is {@code 'n32'}. 'n31' is assumed to be returned from this method then. * <p/> * <b>Note:</b> current method avoids going too deep if found node type is the same as start node type * * @return direct or indirect previous node of the given one having target type if possible; {@code null} otherwise */ private static boolean findPreviousNode(AlignmentInColumnsConfig config, ASTNode from, IElementType targetType, boolean processFrom, boolean processParent, NodeProcessor processor) { if (from == null) return false; for (ASTNode prev = processFrom ? from : from.getTreePrev(); prev != null; prev = prev.getTreePrev()) { IElementType prevType = prev.getElementType(); if (prevType == targetType) { if (processor.targetTypeFound(prev)) return true; } else if (config.getWhiteSpaceTokenTypes().contains(prevType)) { if (processor.whitespaceFound(prev)) return true; } if (findPreviousNode(config, prev.getLastChildNode(), targetType, true, false, processor)) return true; } if (processParent) { for (ASTNode parent = from.getTreeParent(); parent != null; parent = parent.getTreeParent()) { if (findPreviousNode(config, parent, targetType, false, false, processor)) return true; } } return false; } private static abstract class NodeProcessor { public boolean targetTypeFound(ASTNode node) { return false; } public boolean whitespaceFound(ASTNode node) { return false; } } @SuppressWarnings({"StatementWithEmptyBody"}) @Nullable private static ASTNode getSubNodeThatStartsNewLine(@Nullable ASTNode startNode, AlignmentInColumnsConfig config) { if (startNode == null) { return null; } ASTNode parent = startNode.getTreeParent(); if (parent == null) { // Never expect to be here. return null; } // Check if previous node to the start node is white space that contains line feeds. final boolean[] returnFirstNonEmptySubNode = {false}; findPreviousNode(config, startNode, new NodeProcessor() { @Override public boolean targetTypeFound(ASTNode node) { return true; } @Override public boolean whitespaceFound(ASTNode node) { return returnFirstNonEmptySubNode[0] = StringUtil.countNewLines(node.getChars()) > 0; } }); boolean stop = false; for (ASTNode result = startNode; result != null && result.getTreeParent() == parent; result = result.getTreeNext()) { if (config.getStopMultilineCheckElementTypes().contains(result.getElementType())) { return null; } if (result.getTextLength() <= 0) { continue; } if (config.getCommentTokenTypes().contains(result.getElementType())) { continue; } if (config.getWhiteSpaceTokenTypes().contains(result.getElementType()) && StringUtil.countNewLines(result.getChars()) > 0) { stop = true; continue; } if (returnFirstNonEmptySubNode[0]) { return result; } if (stop) { return result; } } return null; } private static List<IElementType> findSubNodeTypes(ASTNode node, TokenSet types) { List<IElementType> foundTypes = new SmartList<>(); for (ASTNode child = node.getFirstChildNode(); child != null && child.getTreeParent() == node; child = child.getTreeNext()) { IElementType type = child.getElementType(); if (types.contains(type)) { foundTypes.add(type); } } return foundTypes; } }