blob: 9fb89e08d339245a14c7013e94f9bb30268b7731 [file] [log] [blame]
/*
* 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'</code> processing.
* <p/>
* <code>'Align in columns'</code> 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</code> if given node should be aligned to the previous one; <code>false</code> 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</code> 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</code> 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'</code>. '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</code> 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<IElementType>();
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;
}
}