| /* |
| * 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<IndentImpl.Type>(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<WrapImpl>(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</code> 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) { |
| // 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()) { |
| anchorBlock = c; |
| break; |
| } |
| } |
| } |
| } |
| 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)); |
| } |
| } |
| } |
| |
| /** |
| * 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</code> if current block has a child that is located before the given block and contains line feed; |
| * <code>false</code> 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 |
| */ |
| protected abstract IndentData getNumberOfSymbolsBeforeBlock(); |
| |
| /** |
| * @return previous block for the current block if any; <code>null</code> 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</code> 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'</code>) 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</code> 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'</code> property of the current wrapped block. |
| * <p/> |
| * Given value is also applied to '<code>indentFromParent'</code> properties of all parents of the current wrapped block if the |
| * value is defined (not <code>null</code>). |
| * <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</code> 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 + ")"; |
| } |
| } |