blob: 427fa184ba8a0d0b8def1739f128f92ba9787dd0 [file] [log] [blame]
/*
* 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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
class AlignmentImpl extends Alignment {
private static final List<LeafBlockWrapper> EMPTY = Collections.unmodifiableList(new ArrayList<LeafBlockWrapper>(0));
private final boolean myAllowBackwardShift;
private final Anchor myAnchor;
private Collection<LeafBlockWrapper> myOffsetRespBlocks = EMPTY;
private AlignmentImpl myParentAlignment;
/**
* Creates new <code>AlignmentImpl</code> object with <code>'false'</code> as <code>'allows backward shift'</code> argument flag.
*/
AlignmentImpl() {
this(false, Anchor.LEFT);
}
/**
* Creates new <code>AlignmentImpl</code> object with the given <code>'allows backward shift'</code> argument flag.
*
* @param allowBackwardShift flag that indicates if it should be possible to shift former aligned block to right
* in order to align to subsequent aligned block (see {@link Alignment#createAlignment(boolean, Anchor)})
* @param anchor alignment anchor (see {@link Alignment#createAlignment(boolean, Anchor)})
*/
AlignmentImpl(boolean allowBackwardShift, @NotNull Anchor anchor) {
myAllowBackwardShift = allowBackwardShift;
myAnchor = anchor;
}
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();
}
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</code> otherwise
*/
@Nullable
LeafBlockWrapper getOffsetRespBlockBefore(@Nullable final AbstractBlockWrapper block) {
if (!continueOffsetResponsibleBlockRetrieval(block)) {
return null;
}
LeafBlockWrapper result = null;
if (myOffsetRespBlocks != EMPTY) {
LeafBlockWrapper lastBlockAfterLineFeed = null;
LeafBlockWrapper firstAlignedBlock = null;
LeafBlockWrapper lastAlignedBlock = null;
for (final LeafBlockWrapper current : myOffsetRespBlocks) {
if (block == null || current.getStartOffset() < block.getStartOffset()) {
if (!onDifferentLines(current, block)) {
continue;
}
if (firstAlignedBlock == null || firstAlignedBlock.getStartOffset() > current.getStartOffset()) {
firstAlignedBlock = current;
}
if (lastAlignedBlock == null || lastAlignedBlock.getStartOffset() < current.getStartOffset()) {
lastAlignedBlock = current;
}
if (current.getWhiteSpace().containsLineFeeds() &&
(lastBlockAfterLineFeed == null || lastBlockAfterLineFeed.getStartOffset() < current.getStartOffset())) {
lastBlockAfterLineFeed = current;
}
}
//each.remove();
}
if (lastBlockAfterLineFeed != null) {
result = lastBlockAfterLineFeed;
}
else if (firstAlignedBlock != null) {
result = firstAlignedBlock;
}
else {
result = lastAlignedBlock;
}
}
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
*/
void setOffsetRespBlock(final LeafBlockWrapper block) {
if (myOffsetRespBlocks == EMPTY) myOffsetRespBlocks = new LinkedHashSet<LeafBlockWrapper>(1);
myOffsetRespBlocks.add(block);
}
@Nullable
private AbstractBlockWrapper getLeftRespNeighbor(@NotNull AbstractBlockWrapper block) {
AbstractBlockWrapper nearLeft = null;
int distance = Integer.MAX_VALUE;
for (AbstractBlockWrapper offsetBlock : myOffsetRespBlocks) if (offsetBlock != null) {
int curDistance = block.getStartOffset() - offsetBlock.getStartOffset();
if (curDistance < distance && curDistance > 0) {
nearLeft = offsetBlock;
distance = curDistance;
}
}
return nearLeft;
}
@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 = 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);
}
}