/* * 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.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; public class AlignmentImpl extends Alignment { private static final List<LeafBlockWrapper> EMPTY = Collections.emptyList(); private final boolean myAllowBackwardShift; private final Anchor myAnchor; private List<LeafBlockWrapper> myOffsetRespBlocks = EMPTY; private AlignmentImpl myParentAlignment; private ProbablyIncreasingLowerboundAlgorithm<LeafBlockWrapper> myOffsetRespBlocksCalculator; AlignmentImpl() { this(false, Anchor.LEFT); } AlignmentImpl(boolean allowBackwardShift, @NotNull Anchor anchor) { myAllowBackwardShift = allowBackwardShift; myAnchor = anchor; myOffsetRespBlocksCalculator = new ProbablyIncreasingLowerboundAlgorithm<>(myOffsetRespBlocks); } public boolean isAllowBackwardShift() { return myAllowBackwardShift; } @NotNull public Anchor getAnchor() { return myAnchor; } public String getId() { return String.valueOf(System.identityHashCode(this)); } public void reset() { if (myOffsetRespBlocks != EMPTY) { myOffsetRespBlocks.clear(); } myOffsetRespBlocksCalculator.reset(); } public void setParent(final Alignment base) { myParentAlignment = (AlignmentImpl)base; } /** * Selects target wrapped block by the following algorithm: * <ol> * <li> * Filter blocks registered via {@link #setOffsetRespBlock(LeafBlockWrapper)} in order to process only those that start * before the given block (blocks which start offset is lower than start offset of the given block). * </li> * <li> * Try to find out result from those filtered blocks using the following algorithm: * <ol> * <li> * Use the last block (block which has the greatest start offset) after the block which * {@link AbstractBlockWrapper#getWhiteSpace() white space} contains line feeds; * </li> * <li> * Use the first block (block with the smallest start offset) if no block can be selected using the rule above; * </li> * <li> * Use the last block (block with the greatest start offset) if no block can be selected using the rules above; * </li> * </ol> * </li> * <li> * Delegate the task to the {@link #setParent(Alignment) parent alignment} (if it's registered) if no blocks * are configured for the current one; * </li> * </ol> * * @param block target block to use during blocks filtering * @return block {@link #setOffsetRespBlock(LeafBlockWrapper) registered} for the current alignment object or * {@link #setParent(Alignment) its parent} using the algorithm above if any; {@code null} otherwise */ @Nullable public LeafBlockWrapper getOffsetRespBlockBefore(@Nullable final AbstractBlockWrapper block) { if (!continueOffsetResponsibleBlockRetrieval(block)) { return null; } LeafBlockWrapper result = null; final List<LeafBlockWrapper> leftBlocks = myOffsetRespBlocksCalculator.getLeftSubList(block); if (!leftBlocks.isEmpty()) { result = leftBlocks.get(0); } if (result == null && myParentAlignment != null) { return myParentAlignment.getOffsetRespBlockBefore(block); } else { return result; } } /** * Registers wrapped block within the current alignment in order to use it for further * {@link #getOffsetRespBlockBefore(AbstractBlockWrapper)} calls processing. * * @param block wrapped block to register within the current alignment object */ public void setOffsetRespBlock(final LeafBlockWrapper block) { if (block == null) { return; } if (myOffsetRespBlocks == EMPTY) { myOffsetRespBlocks = new ArrayList<>(1); myOffsetRespBlocksCalculator.setBlocksList(myOffsetRespBlocks); } myOffsetRespBlocks.add(block); } public Set<LeafBlockWrapper> getOffsetResponsibleBlocks() { return ContainerUtil.newHashSet(myOffsetRespBlocks); } @NotNull private static AbstractBlockWrapper extendBlockFromStart(@NotNull AbstractBlockWrapper block) { while (true) { AbstractBlockWrapper parent = block.getParent(); if (parent != null && parent.getStartOffset() == block.getStartOffset()) { block = parent; } else { return block; } } } @NotNull private static AbstractBlockWrapper extendBlockFromEnd(@NotNull AbstractBlockWrapper block) { while (true) { AbstractBlockWrapper parent = block.getParent(); if (parent != null && parent.getEndOffset() == block.getEndOffset()) { block = parent; } else { return block; } } } private boolean continueOffsetResponsibleBlockRetrieval(@Nullable AbstractBlockWrapper block) { // We don't want to align block that doesn't start new line if it's not configured for 'by columns' alignment. if (!myAllowBackwardShift && block != null && !block.getWhiteSpace().containsLineFeeds()) { return false; } if (block != null) { AbstractBlockWrapper prevAlignBlock = myOffsetRespBlocksCalculator.getLeftRespNeighbor(block); if (!onDifferentLines(prevAlignBlock, block)) { return false; } //blocks are on different lines if (myAllowBackwardShift && myAnchor == Anchor.RIGHT && prevAlignBlock != null && prevAlignBlock.getWhiteSpace().containsLineFeeds() // {prevAlignBlock} starts new indent => can be moved ) { // extend block on position for right align prevAlignBlock = extendBlockFromStart(prevAlignBlock); AbstractBlockWrapper current = block; do { if (current.getStartOffset() < prevAlignBlock.getEndOffset()) { return false; //{prevAlignBlock{current}} | {current}{prevAlignBlock}, no new lines } if (current.getWhiteSpace().containsLineFeeds()) { break; // correct new line was found } else { AbstractBlockWrapper prev = current.getPreviousBlock(); if (prev != null) { prev = extendBlockFromEnd(prev); } current = prev; } } while (current != null); if (current == null) { return false; //root block is the top } } } return myParentAlignment == null || myParentAlignment.continueOffsetResponsibleBlockRetrieval(block); } private static boolean onDifferentLines(AbstractBlockWrapper block1, AbstractBlockWrapper block2) { if (block1 == null || block2 == null) { return true; } AbstractBlockWrapper leftBlock = block1.getStartOffset() <= block2.getStartOffset() ? block1 : block2; AbstractBlockWrapper rightBlock = block1.getStartOffset() > block2.getStartOffset() ? block1 : block2; for (; rightBlock != null && rightBlock.getStartOffset() > leftBlock.getStartOffset(); rightBlock = rightBlock.getPreviousBlock()) { if (rightBlock.getWhiteSpace().containsLineFeeds()) { return true; } } return false; } @Override public String toString() { return "Align: " + System.identityHashCode(this) + "," + getAnchor() + (isAllowBackwardShift() ? "<" : ""); } }