/*
* Copyright 2000-2015 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.engine;
import com.intellij.formatting.*;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
public class WrapProcessor {
private LeafBlockWrapper myFirstWrappedBlockOnLine = null;
private BlockRangesMap myBlockRangesMap;
private LeafBlockWrapper myWrapCandidate = null;
private IndentAdjuster myIndentAdjuster;
private int myRightMargin;
public WrapProcessor(BlockRangesMap blockHelper, IndentAdjuster indentAdjuster, int rightMargin) {
myIndentAdjuster = indentAdjuster;
myBlockRangesMap = blockHelper;
myRightMargin = rightMargin;
}
private boolean isSuitableInTheCurrentPosition(final WrapImpl wrap, LeafBlockWrapper currentBlock) {
if (wrap.getWrapOffset() < currentBlock.getStartOffset()) {
return true;
}
if (wrap.isWrapFirstElement()) {
return true;
}
if (wrap.getType() == WrapImpl.Type.WRAP_AS_NEEDED) {
return positionAfterWrappingIsSuitable(currentBlock);
}
return wrap.getType() == WrapImpl.Type.CHOP_IF_NEEDED && lineOver(currentBlock) && positionAfterWrappingIsSuitable(currentBlock);
}
private boolean lineOver(LeafBlockWrapper currentBlock) {
return !currentBlock.containsLineFeeds() &&
CoreFormatterUtil.getStartColumn(currentBlock) + currentBlock.getLength() > myRightMargin;
}
/**
* Ensures that offset of the currentBlock is not increased if we make a wrap on it.
*/
private boolean positionAfterWrappingIsSuitable(LeafBlockWrapper currentBlock) {
final WhiteSpace whiteSpace = currentBlock.getWhiteSpace();
if (whiteSpace.containsLineFeeds()) return true;
final int spaces = whiteSpace.getSpaces();
int indentSpaces = whiteSpace.getIndentSpaces();
try {
final int startColumnNow = CoreFormatterUtil.getStartColumn(currentBlock);
whiteSpace.ensureLineFeed();
myIndentAdjuster.adjustLineIndent(currentBlock);
final int startColumnAfterWrap = CoreFormatterUtil.getStartColumn(currentBlock);
return startColumnNow > startColumnAfterWrap;
}
finally {
whiteSpace.removeLineFeeds(currentBlock.getSpaceProperty(), myBlockRangesMap);
whiteSpace.setSpaces(spaces, indentSpaces);
}
}
@Nullable
private WrapImpl getWrapToBeUsed(final ArrayList<WrapImpl> wraps, LeafBlockWrapper currentBlock) {
if (wraps.isEmpty()) {
return null;
}
if (myWrapCandidate == currentBlock) return wraps.get(0);
for (final WrapImpl wrap : wraps) {
if (!isSuitableInTheCurrentPosition(wrap, currentBlock)) continue;
if (wrap.isActive()) return wrap;
final WrapImpl.Type type = wrap.getType();
if (type == WrapImpl.Type.WRAP_ALWAYS) return wrap;
if (type == WrapImpl.Type.WRAP_AS_NEEDED || type == WrapImpl.Type.CHOP_IF_NEEDED) {
if (lineOver(currentBlock)) {
return wrap;
}
}
}
return null;
}
private boolean isCandidateToBeWrapped(final WrapImpl wrap, LeafBlockWrapper currentBlock) {
return isSuitableInTheCurrentPosition(wrap, currentBlock) &&
(wrap.getType() == WrapImpl.Type.WRAP_AS_NEEDED || wrap.getType() == WrapImpl.Type.CHOP_IF_NEEDED) &&
!currentBlock.getWhiteSpace().isReadOnly();
}
/**
* Allows to answer if wrap of the {@link #myWrapCandidate} object (if any) may be replaced by the given wrap.
*
* @param wrap wrap candidate to check
* @return {@code true} if wrap of the {@link #myWrapCandidate} object (if any) may be replaced by the given wrap;
* {@code false} otherwise
*/
private boolean canReplaceWrapCandidate(WrapImpl wrap, LeafBlockWrapper currentBlock) {
if (myWrapCandidate == null) return true;
WrapImpl.Type type = wrap.getType();
if (wrap.isActive() && (type == WrapImpl.Type.CHOP_IF_NEEDED || type == WrapImpl.Type.WRAP_ALWAYS)) return true;
final WrapImpl currentWrap = myWrapCandidate.getWrap();
return wrap == currentWrap || !wrap.isChildOf(currentWrap, currentBlock);
}
public LeafBlockWrapper processWrap(LeafBlockWrapper currentBlock) {
final SpacingImpl spacing = currentBlock.getSpaceProperty();
final WhiteSpace whiteSpace = currentBlock.getWhiteSpace();
final boolean wrapWasPresent = whiteSpace.containsLineFeeds();
if (wrapWasPresent) {
myFirstWrappedBlockOnLine = null;
if (!whiteSpace.containsLineFeedsInitially()) {
whiteSpace.removeLineFeeds(spacing, myBlockRangesMap);
}
}
final boolean wrapIsPresent = whiteSpace.containsLineFeeds();
final ArrayList<WrapImpl> wraps = currentBlock.getWraps();
for (WrapImpl wrap : wraps) {
wrap.setWrapOffset(currentBlock.getStartOffset());
}
final WrapImpl wrap = getWrapToBeUsed(wraps, currentBlock);
if (wrap != null || wrapIsPresent) {
if (!wrapIsPresent && !canReplaceWrapCandidate(wrap, currentBlock)) {
return myWrapCandidate;
}
if (wrap != null && wrap.getChopStartBlock() != null) {
// getWrapToBeUsed() returns the block only if it actually exceeds the right margin. In this case, we need to go back to the
// first block that has the CHOP_IF_NEEDED wrap type and start wrapping from there.
LeafBlockWrapper newCurrentBlock = wrap.getChopStartBlock();
wrap.setActive();
return newCurrentBlock;
}
if (wrap != null && isChopNeeded(wrap, currentBlock)) {
wrap.setActive();
}
if (!wrapIsPresent) {
whiteSpace.ensureLineFeed();
if (!wrapWasPresent) {
if (myFirstWrappedBlockOnLine != null && wrap.isChildOf(myFirstWrappedBlockOnLine.getWrap(), currentBlock)) {
wrap.ignoreParentWrap(myFirstWrappedBlockOnLine.getWrap(), currentBlock);
return myFirstWrappedBlockOnLine;
}
else {
myFirstWrappedBlockOnLine = currentBlock;
}
}
}
myWrapCandidate = null;
}
else {
for (final WrapImpl wrap1 : wraps) {
if (isCandidateToBeWrapped(wrap1, currentBlock) && canReplaceWrapCandidate(wrap1, currentBlock)) {
myWrapCandidate = currentBlock;
}
if (isChopNeeded(wrap1, currentBlock)) {
wrap1.saveChopBlock(currentBlock);
}
}
}
if (!whiteSpace.containsLineFeeds() && myWrapCandidate != null && !whiteSpace.isReadOnly() && lineOver(currentBlock)) {
return myWrapCandidate;
}
return null;
}
private boolean isChopNeeded(final WrapImpl wrap, LeafBlockWrapper currentBlock) {
return wrap != null && wrap.getType() == WrapImpl.Type.CHOP_IF_NEEDED && isSuitableInTheCurrentPosition(wrap, currentBlock);
}
public void reset() {
myWrapCandidate = null;
}
public void onCurrentLineChanged() {
myWrapCandidate = null;
}
}