/* * Copyright 2000-2012 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; import com.intellij.lang.ASTNode; import com.intellij.lang.Language; import com.intellij.openapi.util.TextRange; import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import static java.util.Arrays.asList; /** * @author lesya */ public abstract class AbstractBlockWrapper { private static final Set<IndentImpl.Type> RELATIVE_INDENT_TYPES = new HashSet<>(asList( Indent.Type.NORMAL, Indent.Type.CONTINUATION, Indent.Type.CONTINUATION_WITHOUT_FIRST )); protected WhiteSpace myWhiteSpaceBefore; protected CompositeBlockWrapper myParent; protected int myStart; protected int myEnd; protected int myFlags; static int CAN_USE_FIRST_CHILD_INDENT_AS_BLOCK_INDENT = 1; static int INCOMPLETE = 2; private final Language myLanguage; protected IndentInfo myIndentFromParent = null; private IndentImpl myIndent = null; private AlignmentImpl myAlignment; private WrapImpl myWrap; private final ASTNode myNode; public AbstractBlockWrapper(final Block block, final WhiteSpace whiteSpaceBefore, final CompositeBlockWrapper parent, final TextRange textRange) { myWhiteSpaceBefore = whiteSpaceBefore; myParent = parent; myStart = textRange.getStartOffset(); myEnd = textRange.getEndOffset(); myFlags = CAN_USE_FIRST_CHILD_INDENT_AS_BLOCK_INDENT | (block.isIncomplete() ? INCOMPLETE : 0); myAlignment = (AlignmentImpl)block.getAlignment(); myWrap = (WrapImpl)block.getWrap(); myLanguage = deriveLanguage(block); myNode = block instanceof ASTBlock ? ((ASTBlock) block).getNode() : null; } @Nullable private static Language deriveLanguage(@NotNull Block block) { if (block instanceof BlockEx) { return ((BlockEx)block).getLanguage(); } return null; } /** * Returns the whitespace preceding the block. * * @return the whitespace preceding the block */ public WhiteSpace getWhiteSpace() { return myWhiteSpaceBefore; } /** * Returns the AST node corresponding to the block, if known. * * @return the AST node or null */ @Nullable public ASTNode getNode() { return myNode; } /** * Returns the list of wraps for this block and its parent blocks that start at the same offset. The returned list is ordered from top * to bottom (higher-level wraps go first). Stops if the wrap for a block is marked as "ignore parent wraps". * * @return the list of wraps. */ public ArrayList<WrapImpl> getWraps() { final ArrayList<WrapImpl> result = new ArrayList<>(3); AbstractBlockWrapper current = this; while(current != null && current.getStartOffset() == getStartOffset()) { final WrapImpl wrap = current.getOwnWrap(); if (wrap != null && !result.contains(wrap)) result.add(0, wrap); if (wrap != null && wrap.getIgnoreParentWraps()) break; current = current.myParent; } return result; } public int getStartOffset() { return myStart; } public int getEndOffset() { return myEnd; } public int getLength() { return myEnd - myStart; } /** * There is a possible case that particular block's language differs from the language implied by the file type. We need to * distinguish such a situation because, for example in case of indent calculation (code style settings for different languages * may have different indent values). * <p/> * This method allows to retrieve the language associated with the current block (if provided). * * @return current block's language (if provided) */ @Nullable public Language getLanguage() { return myLanguage; } /** * Applies given start offset to the current block wrapper and recursively calls this method on parent block wrapper * if it starts at the same place as the current one. * * @param startOffset new start offset value to apply */ protected void arrangeStartOffset(final int startOffset) { if (getStartOffset() == startOffset) return; boolean isFirst = getParent() != null && getStartOffset() == getParent().getStartOffset(); myStart = startOffset; if (isFirst) { getParent().arrangeStartOffset(startOffset); } } public IndentImpl getIndent(){ return myIndent; } public CompositeBlockWrapper getParent() { return myParent; } @Nullable public WrapImpl getWrap() { final ArrayList<WrapImpl> wraps = getWraps(); if (wraps.size() == 0) return null; return wraps.get(0); } /** * @return wrap object configured for the current block wrapper if any; {@code null} otherwise */ public WrapImpl getOwnWrap() { return myWrap; } public void reset() { myFlags |= CAN_USE_FIRST_CHILD_INDENT_AS_BLOCK_INDENT; final AlignmentImpl alignment = myAlignment; if (alignment != null) alignment.reset(); final WrapImpl wrap = myWrap; if (wrap != null) wrap.reset(); } public IndentData getChildOffset(AbstractBlockWrapper child, CommonCodeStyleSettings.IndentOptions options, int targetBlockStartOffset) { final boolean childStartsNewLine = child.getWhiteSpace().containsLineFeeds(); IndentImpl.Type childIndentType = child.getIndent().getType(); IndentData childIndent; // Calculate child indent. if (childStartsNewLine || (!getWhiteSpace().containsLineFeeds() && RELATIVE_INDENT_TYPES.contains(childIndentType) && indentAlreadyUsedBefore(child))) { childIndent = CoreFormatterUtil.getIndent(options, child, targetBlockStartOffset); } else if (child.getIndent().isEnforceIndentToChildren() && !child.getWhiteSpace().containsLineFeeds()) { // Enforce indent if child doesn't start new line, e.g. prefer the code below: // void test() { // foo("test", new Runnable() { // public void run() { // } // }, // new Runnable() { // public void run() { // } // } // ); // } // to this one: // void test() { // foo("test", new Runnable() { // public void run() { // } // }, // new Runnable() { // public void run() { // } // } // ); // } AlignmentImpl alignment = child.getAlignment(); if (alignment != null) { AbstractBlockWrapper anchorBlock = getAnchorBlock(child, targetBlockStartOffset, alignment); return anchorBlock.getNumberOfSymbolsBeforeBlock(); } childIndent = CoreFormatterUtil.getIndent(options, child, getStartOffset()); } else { childIndent = new IndentData(0); } // Use child indent if it's absolute and the child is contained on new line. if (childStartsNewLine) { if (child.getIndent().isAbsolute()) { myFlags &= ~CAN_USE_FIRST_CHILD_INDENT_AS_BLOCK_INDENT; AbstractBlockWrapper current = this; while (current != null && current.getStartOffset() == getStartOffset()) { current.myFlags &= ~CAN_USE_FIRST_CHILD_INDENT_AS_BLOCK_INDENT; current = current.myParent; } return childIndent; } else if (child.getIndent().isRelativeToDirectParent() && child.getStartOffset() > getStartOffset()) { return childIndent.add(getNumberOfSymbolsBeforeBlock()); } } if (child.getStartOffset() == getStartOffset()) { final boolean newValue = (myFlags & CAN_USE_FIRST_CHILD_INDENT_AS_BLOCK_INDENT) != 0 && (child.myFlags & CAN_USE_FIRST_CHILD_INDENT_AS_BLOCK_INDENT) != 0 && childIndent.isEmpty(); setCanUseFirstChildIndentAsBlockIndent(newValue); } if (getStartOffset() == targetBlockStartOffset) { if (myParent == null) { return childIndent; } else { return childIndent.add(myParent.getChildOffset(this, options, targetBlockStartOffset)); } } else if (!getWhiteSpace().containsLineFeeds()) { final IndentData indent = createAlignmentIndent(childIndent, child); if (indent != null) { return indent; } return childIndent.add(myParent.getChildOffset(this, options, targetBlockStartOffset)); } else { if (myParent == null) return childIndent.add(getWhiteSpace()); if (getIndent().isAbsolute()) { if (myParent.myParent != null) { return childIndent.add(myParent.myParent.getChildOffset(myParent, options, targetBlockStartOffset)); } else { return childIndent.add(getWhiteSpace()); } } if ((myFlags & CAN_USE_FIRST_CHILD_INDENT_AS_BLOCK_INDENT) != 0) { final IndentData indent = createAlignmentIndent(childIndent, child); if (indent != null) { return indent; } return childIndent.add(getWhiteSpace()); } else { return childIndent.add(myParent.getChildOffset(this, options, targetBlockStartOffset)); } } } @NotNull private AbstractBlockWrapper getAnchorBlock(AbstractBlockWrapper child, int targetBlockStartOffset, AlignmentImpl alignment) { // Generally, we want to handle situation like the one below: // test("text", new Runnable() { // @Override // public void run() { // } // }, // new Runnable() { // @Override // public void run() { // } // } // ); // I.e. we want 'run()' method from the first anonymous class to be aligned with the 'run()' method of the second anonymous class. AbstractBlockWrapper anchorBlock = alignment.getOffsetRespBlockBefore(child); if (anchorBlock == null) { anchorBlock = this; if (anchorBlock instanceof CompositeBlockWrapper) { List<AbstractBlockWrapper> children = ((CompositeBlockWrapper)anchorBlock).getChildren(); for (AbstractBlockWrapper c : children) { if (c.getStartOffset() != getStartOffset() && c.getStartOffset() < targetBlockStartOffset) { anchorBlock = c; break; } } } } return anchorBlock; } /** * Allows to answer if current wrapped block has a child block that is located before given block and has line feed. * * @param child target child block to process * @return {@code true} if current block has a child that is located before the given block and contains line feed; * {@code false} otherwise */ protected abstract boolean indentAlreadyUsedBefore(final AbstractBlockWrapper child); /** * Allows to retrieve object that encapsulates information about number of symbols before the current block starting * from the line start. I.e. all symbols (either white space or not) between start of the line where current block begins * and the block itself are count and returned. * * @return object that encapsulates information about number of symbols before the current block */ public abstract IndentData getNumberOfSymbolsBeforeBlock(); /** * @return previous block for the current block if any; {@code null} otherwise */ @Nullable protected abstract LeafBlockWrapper getPreviousBlock(); protected final void setCanUseFirstChildIndentAsBlockIndent(final boolean newValue) { if (newValue) myFlags |= CAN_USE_FIRST_CHILD_INDENT_AS_BLOCK_INDENT; else myFlags &= ~CAN_USE_FIRST_CHILD_INDENT_AS_BLOCK_INDENT; } /** * Applies start offset of the current block wrapper to the parent block wrapper if the one is defined. */ public void arrangeParentTextRange() { if (myParent != null) { myParent.arrangeStartOffset(getStartOffset()); } } public IndentData calculateChildOffset(final CommonCodeStyleSettings.IndentOptions indentOption, final ChildAttributes childAttributes, int index) { IndentImpl childIndent = (IndentImpl)childAttributes.getChildIndent(); if (childIndent == null) childIndent = (IndentImpl)Indent.getContinuationWithoutFirstIndent(indentOption.USE_RELATIVE_INDENTS); IndentData indent = getIndent(indentOption, index, childIndent); if (childIndent.isRelativeToDirectParent()) { return new IndentData(indent.getIndentSpaces() + CoreFormatterUtil.getStartColumn(CoreFormatterUtil.getFirstLeaf(this)), indent.getSpaces()); } if (myParent == null || (myFlags & CAN_USE_FIRST_CHILD_INDENT_AS_BLOCK_INDENT) != 0 && getWhiteSpace().containsLineFeeds()) { return indent.add(getWhiteSpace()); } else { IndentData offsetFromParent = myParent.getChildOffset(this, indentOption, -1); return indent.add(offsetFromParent); } } /** * Allows to retrieve alignment applied to any block that conforms to the following conditions: * <p/> * <ul> * <li>that block is current block or its ancestor (direct or indirect parent);</li> * <li>that block starts at the same offset as the current one;</li> * </ul> * * @return alignment of the current block or it's ancestor that starts at the same offset as the current if any; * {@code null} otherwise */ @Nullable public AlignmentImpl getAlignmentAtStartOffset() { for (AbstractBlockWrapper block = this; block != null && block.getStartOffset() == getStartOffset(); block = block.getParent()) { if (block.getAlignment() != null) { return block.getAlignment(); } } return null; } /** * Check if it's possible to construct indent for the block that is affected by aligning rules. E.g. there is a possible case * that the user configures method call arguments to be aligned and single parameter expression spans more than one line: * <p/> * <pre> * public void test(String s1, String s2) {} * * public void foo() { * test("11" * + "12" * + "13", * "21" * + "22"); * } * </pre> * <p/> * Here both composite blocks (<b>"11" + "12" + "13"</b> and <b>"21" + "22"</b>) are aligned as method call argument but their * sub-blocks that are located on new lines should also be indented to the point of composite block start. * <p/> * This method takes care about constructing target absolute indent of the given child block assuming that it's parent * (referenced by {@code 'this'}) or it's ancestor that starts at the same offset is aligned. * * @param indentFromParent basic indent of given child from the current parent block * @param child child block of the current aligned composite block * @return absolute indent to use for the given child block of the current composite block if alignment-affected * indent should be used for it; * {@code null} otherwise */ @Nullable private IndentData createAlignmentIndent(IndentData indentFromParent, AbstractBlockWrapper child) { if (!child.getWhiteSpace().containsLineFeeds()) { return null; } AlignmentImpl alignment = getAlignmentAtStartOffset(); if (alignment == null || alignment == child.getAlignment()) { return null; } AbstractBlockWrapper previous = child.getPreviousBlock(); LeafBlockWrapper anchorOffsetBlock = alignment.getOffsetRespBlockBefore(child); if (anchorOffsetBlock != null && anchorOffsetBlock.getStartOffset() != getStartOffset()) { // Located on different lines. boolean onDifferentLines = false; for (LeafBlockWrapper b = anchorOffsetBlock.getNextBlock(); b != null && b.getStartOffset() < getStartOffset(); b = b.getNextBlock()) { if (b.getWhiteSpace().containsLineFeeds()) { onDifferentLines = true; break; } } if (!onDifferentLines) { return null; } } // There is no point in continuing processing if given child is the first block, i.e. there is no alignment-implied // offset to add to the given 'indent from parent'. if (previous == null) { return indentFromParent; } IndentData symbolsBeforeCurrent; if (anchorOffsetBlock == null) { symbolsBeforeCurrent = getNumberOfSymbolsBeforeBlock(); } else { symbolsBeforeCurrent = anchorOffsetBlock.getNumberOfSymbolsBeforeBlock(); } // Result is calculated as a number of symbols between the current composite parent block plus given 'indent from parent'. int indentSpaces = symbolsBeforeCurrent.getIndentSpaces() + indentFromParent.getSpaces() + indentFromParent.getIndentSpaces(); return new IndentData(indentSpaces, symbolsBeforeCurrent.getSpaces()); } private static IndentData getIndent(final CommonCodeStyleSettings.IndentOptions options, final int index, IndentImpl indent) { if (indent.getType() == Indent.Type.CONTINUATION) { return new IndentData(options.CONTINUATION_INDENT_SIZE); } if (indent.getType() == Indent.Type.CONTINUATION_WITHOUT_FIRST) { if (index != 0) { return new IndentData(options.CONTINUATION_INDENT_SIZE); } else { return new IndentData(0); } } if (indent.getType() == Indent.Type.LABEL) return new IndentData(options.LABEL_INDENT_SIZE); if (indent.getType() == Indent.Type.NONE) return new IndentData(0); if (indent.getType() == Indent.Type.SPACES) return new IndentData(indent.getSpaces(), 0); return new IndentData(options.INDENT_SIZE); } /** * Applies given indent value to '{@code indentFromParent'} property of the current wrapped block. * <p/> * Given value is also applied to '{@code indentFromParent'} properties of all parents of the current wrapped block if the * value is defined (not {@code null}). * <p/> * This property is used later during * {@link LeafBlockWrapper#calculateOffset(CommonCodeStyleSettings.IndentOptions)} leaf block offset calculation}. * * @param indentFromParent indent value to apply */ public void setIndentFromParent(final IndentInfo indentFromParent) { myIndentFromParent = indentFromParent; if (myIndentFromParent != null) { AbstractBlockWrapper parent = myParent; if (myParent != null && myParent.getStartOffset() == myStart) { parent.setIndentFromParent(myIndentFromParent); } } } /** * Tries to find first parent block of the current block that starts before the current block and which * {@link WhiteSpace white space} contains line feeds. * * @return first parent block that starts before the current block and which white space contains line feeds if any; * {@code null} otherwise */ @Nullable protected AbstractBlockWrapper findFirstIndentedParent() { if (myParent == null) return null; if (myStart != myParent.getStartOffset() && myParent.getWhiteSpace().containsLineFeeds()) return myParent; return myParent.findFirstIndentedParent(); } public void setIndent(@Nullable final IndentImpl indent) { myIndent = indent; } public AlignmentImpl getAlignment() { return myAlignment; } public boolean isIncomplete() { return (myFlags & INCOMPLETE) != 0; } public void dispose() { myAlignment = null; myWrap = null; myIndent = null; myIndentFromParent = null; myParent = null; myWhiteSpaceBefore = null; } @Override public String toString() { return getClass().getName() + "(" + myStart + "-" + myEnd + ")"; } }