/*
* 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.formatting.engine.ExpandableIndent;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.LinkedMultiMap;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.containers.Stack;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Allows to build {@link AbstractBlockWrapper formatting block wrappers} for the target {@link Block formatting blocks}.
* The main idea of block wrapping is to associate information about {@link WhiteSpace white space before block} with the block itself.
*/
public class InitialInfoBuilder {
private static final RangesAssert ASSERT = new RangesAssert();
private static final boolean INLINE_TABS_ENABLED = "true".equalsIgnoreCase(System.getProperty("inline.tabs.enabled"));
private final Map<AbstractBlockWrapper, Block> myResult = new THashMap<>();
private MultiMap<ExpandableIndent, AbstractBlockWrapper> myBlocksToForceChildrenIndent = new LinkedMultiMap<>();
private MultiMap<Alignment, Block> myBlocksToAlign = new MultiMap<>();
private Set<Alignment> myAlignmentsInsideRangeToModify = ContainerUtil.newHashSet();
private boolean myCollectAlignmentsInsideFormattingRange = false;
private final FormattingDocumentModel myModel;
private final FormatTextRanges myAffectedRanges;
private final List<TextRange> myExtendedAffectedRanges;
private final int myPositionOfInterest;
private final FormattingProgressCallback myProgressCallback;
private final FormatterTagHandler myFormatterTagHandler;
private final CommonCodeStyleSettings.IndentOptions myOptions;
private final Stack<InitialInfoBuilderState> myStates = new Stack<>();
private WhiteSpace myCurrentWhiteSpace;
private CompositeBlockWrapper myRootBlockWrapper;
private LeafBlockWrapper myPreviousBlock;
private LeafBlockWrapper myFirstTokenBlock;
private LeafBlockWrapper myLastTokenBlock;
private SpacingImpl myCurrentSpaceProperty;
private boolean myInsideFormatRestrictingTag;
private InitialInfoBuilder(final Block rootBlock,
final FormattingDocumentModel model,
@Nullable final FormatTextRanges affectedRanges,
@NotNull CodeStyleSettings settings,
final CommonCodeStyleSettings.IndentOptions options,
final int positionOfInterest,
@NotNull FormattingProgressCallback progressCallback)
{
myModel = model;
myAffectedRanges = affectedRanges;
myExtendedAffectedRanges = affectedRanges != null ? affectedRanges.getExtendedFormattingRanges() : null;
myProgressCallback = progressCallback;
myCurrentWhiteSpace = new WhiteSpace(getStartOffset(rootBlock), true);
myOptions = options;
myPositionOfInterest = positionOfInterest;
myInsideFormatRestrictingTag = false;
myFormatterTagHandler = new FormatterTagHandler(settings);
}
protected static InitialInfoBuilder prepareToBuildBlocksSequentially(
Block root,
FormattingDocumentModel model,
FormatProcessor.FormatOptions formatOptions,
CodeStyleSettings settings,
CommonCodeStyleSettings.IndentOptions options,
@NotNull FormattingProgressCallback progressCallback)
{
InitialInfoBuilder builder = new InitialInfoBuilder(root, model, formatOptions.myAffectedRanges, settings, options, formatOptions.myInterestingOffset, progressCallback);
builder.setCollectAlignmentsInsideFormattingRange(formatOptions.myReformatContext);
builder.buildFrom(root, 0, null, null, null);
return builder;
}
private int getStartOffset(@NotNull Block rootBlock) {
int minOffset = rootBlock.getTextRange().getStartOffset();
if (myAffectedRanges != null) {
for (FormatTextRange range : myAffectedRanges.getRanges()) {
if (range.getStartOffset() < minOffset) minOffset = range.getStartOffset();
}
}
return minOffset;
}
public FormattingDocumentModel getFormattingDocumentModel() {
return myModel;
}
public int getEndOffset() {
int maxDocOffset = myModel.getTextLength();
int maxOffset = myRootBlockWrapper != null ? myRootBlockWrapper.getEndOffset() : 0;
if (myAffectedRanges != null) {
for (FormatTextRange range : myAffectedRanges.getRanges()) {
if (range.getTextRange().getEndOffset() > maxOffset) maxOffset = range.getTextRange().getEndOffset();
}
}
return maxOffset < maxDocOffset ? maxOffset : maxDocOffset;
}
public boolean iteration() {
if (myStates.isEmpty()) {
return true;
}
InitialInfoBuilderState state = myStates.peek();
doIteration(state);
return myStates.isEmpty();
}
private AbstractBlockWrapper buildFrom(final Block rootBlock,
final int index,
@Nullable final CompositeBlockWrapper parent,
@Nullable WrapImpl currentWrapParent,
@Nullable final Block parentBlock)
{
final WrapImpl wrap = (WrapImpl)rootBlock.getWrap();
if (wrap != null) {
wrap.registerParent(currentWrapParent);
currentWrapParent = wrap;
}
TextRange textRange = rootBlock.getTextRange();
final int blockStartOffset = textRange.getStartOffset();
if (parent != null) {
checkRanges(parent, textRange);
}
myCurrentWhiteSpace.changeEndOffset(blockStartOffset, myModel, myOptions);
collectAlignments(rootBlock);
if (isInsideFormattingRanges(rootBlock) || shouldCollectAlignmentsAround(rootBlock)) {
final List<Block> subBlocks = rootBlock.getSubBlocks();
if (subBlocks.isEmpty()) {
final AbstractBlockWrapper wrapper = buildLeafBlock(rootBlock, parent, false, index, parentBlock);
if (!subBlocks.isEmpty()) {
wrapper.setIndent((IndentImpl)subBlocks.get(0).getIndent());
}
return wrapper;
}
return buildCompositeBlock(rootBlock, parent, index, currentWrapParent);
}
else {
return buildLeafBlock(rootBlock, parent, true, index, parentBlock);
}
}
private boolean shouldCollectAlignmentsAround(Block rootBlock) {
return myCollectAlignmentsInsideFormattingRange && isInsideExtendedAffectedRange(rootBlock);
}
private void collectAlignments(Block rootBlock) {
if (myCollectAlignmentsInsideFormattingRange && rootBlock.getAlignment() != null
&& isAffectedByFormatting(rootBlock) && !myInsideFormatRestrictingTag)
{
myAlignmentsInsideRangeToModify.add(rootBlock.getAlignment());
}
if (rootBlock.getAlignment() != null) {
myBlocksToAlign.putValue(rootBlock.getAlignment(), rootBlock);
}
}
private void checkRanges(@NotNull CompositeBlockWrapper parent, TextRange textRange) {
if (textRange.getStartOffset() < parent.getStartOffset()) {
ASSERT.assertInvalidRanges(
textRange.getStartOffset(),
parent.getStartOffset(),
myModel,
"child block start is less than parent block start"
);
}
if (textRange.getEndOffset() > parent.getEndOffset()) {
ASSERT.assertInvalidRanges(
textRange.getEndOffset(),
parent.getEndOffset(),
myModel,
"child block end is after parent block end"
);
}
}
private boolean isInsideExtendedAffectedRange(Block rootBlock) {
if (myExtendedAffectedRanges == null) return false;
TextRange blockRange = rootBlock.getTextRange();
for (TextRange affectedRange : myExtendedAffectedRanges) {
if (affectedRange.intersects(blockRange)) return true;
}
return false;
}
private CompositeBlockWrapper buildCompositeBlock(Block rootBlock,
@Nullable CompositeBlockWrapper parent,
int index,
@Nullable WrapImpl currentWrapParent)
{
final CompositeBlockWrapper wrappedRootBlock = new CompositeBlockWrapper(rootBlock, myCurrentWhiteSpace, parent);
if (index == 0) {
wrappedRootBlock.arrangeParentTextRange();
}
if (myRootBlockWrapper == null) {
myRootBlockWrapper = wrappedRootBlock;
myRootBlockWrapper.setIndent((IndentImpl)Indent.getNoneIndent());
}
boolean blocksMayBeOfInterest = false;
if (myPositionOfInterest != -1) {
myResult.put(wrappedRootBlock, rootBlock);
blocksMayBeOfInterest = true;
}
final boolean blocksAreReadOnly = rootBlock instanceof ReadOnlyBlockContainer || blocksMayBeOfInterest;
InitialInfoBuilderState state = new InitialInfoBuilderState(rootBlock, wrappedRootBlock, currentWrapParent, blocksAreReadOnly);
myStates.push(state);
return wrappedRootBlock;
}
private void doIteration(@NotNull InitialInfoBuilderState state) {
Block currentRoot = state.parentBlock;
List<Block> subBlocks = currentRoot.getSubBlocks();
int currentBlockIndex = state.getIndexOfChildBlockToProcess();
final Block currentBlock = subBlocks.get(currentBlockIndex);
initCurrentWhiteSpace(currentRoot, state.previousBlock, currentBlock);
final AbstractBlockWrapper wrapper = buildFrom(
currentBlock, currentBlockIndex, state.wrappedBlock, state.parentBlockWrap, currentRoot
);
registerExpandableIndents(currentBlock, wrapper);
if (wrapper.getIndent() == null) {
wrapper.setIndent((IndentImpl)currentBlock.getIndent());
}
if (!state.readOnly) {
try {
subBlocks.set(currentBlockIndex, null); // to prevent extra strong refs during model building
} catch (Throwable ex) {
// read-only blocks
}
}
if (state.childBlockProcessed(currentBlock, wrapper, myOptions)) {
while (!myStates.isEmpty() && myStates.peek().isProcessed()) {
myStates.pop();
}
}
}
private void initCurrentWhiteSpace(@NotNull Block currentRoot, @Nullable Block previousBlock, @NotNull Block currentBlock) {
if (previousBlock != null || (myCurrentWhiteSpace != null && myCurrentWhiteSpace.isIsFirstWhiteSpace())) {
myCurrentSpaceProperty = (SpacingImpl)currentRoot.getSpacing(previousBlock, currentBlock);
}
}
private void registerExpandableIndents(@NotNull Block block, @NotNull AbstractBlockWrapper wrapper) {
if (block.getIndent() instanceof ExpandableIndent) {
ExpandableIndent indent = (ExpandableIndent)block.getIndent();
myBlocksToForceChildrenIndent.putValue(indent, wrapper);
}
}
private AbstractBlockWrapper buildLeafBlock(final Block rootBlock,
@Nullable final CompositeBlockWrapper parent,
final boolean readOnly,
final int index,
@Nullable Block parentBlock)
{
LeafBlockWrapper result = doProcessSimpleBlock(rootBlock, parent, readOnly, index, parentBlock);
myProgressCallback.afterWrappingBlock(result);
return result;
}
private LeafBlockWrapper doProcessSimpleBlock(final Block rootBlock,
@Nullable final CompositeBlockWrapper parent,
final boolean readOnly,
final int index,
@Nullable Block parentBlock)
{
if (!INLINE_TABS_ENABLED && !myCurrentWhiteSpace.containsLineFeeds()) {
myCurrentWhiteSpace.setForceSkipTabulationsUsage(true);
}
LeafBlockWrapper info = new LeafBlockWrapper(rootBlock, parent, myCurrentWhiteSpace, myModel, myOptions, myPreviousBlock, readOnly);
if (index == 0) {
info.arrangeParentTextRange();
}
checkInsideFormatterOffTag(rootBlock);
TextRange textRange = rootBlock.getTextRange();
if (myPreviousBlock != null) {
myPreviousBlock.setNextBlock(info);
}
if (myFirstTokenBlock == null) {
myFirstTokenBlock = info;
}
myLastTokenBlock = info;
if (currentWhiteSpaceIsReadOnly()) {
myCurrentWhiteSpace.setReadOnly(true);
}
if (myCurrentSpaceProperty != null) {
myCurrentWhiteSpace.setIsSafe(myCurrentSpaceProperty.isSafe());
myCurrentWhiteSpace.setKeepFirstColumn(myCurrentSpaceProperty.shouldKeepFirstColumn());
}
if (info.isEndOfCodeBlock()) {
myCurrentWhiteSpace.setBeforeCodeBlockEnd(true);
}
info.setSpaceProperty(myCurrentSpaceProperty);
myCurrentWhiteSpace = new WhiteSpace(textRange.getEndOffset(), false);
if (myInsideFormatRestrictingTag) myCurrentWhiteSpace.setReadOnly(true);
myPreviousBlock = info;
if (myPositionOfInterest != -1 && (textRange.contains(myPositionOfInterest) || textRange.getEndOffset() == myPositionOfInterest)) {
myResult.put(info, rootBlock);
if (parent != null) myResult.put(parent, parentBlock);
}
return info;
}
private void checkInsideFormatterOffTag(Block rootBlock) {
switch (myFormatterTagHandler.getFormatterTag(rootBlock)) {
case ON:
myInsideFormatRestrictingTag = false;
break;
case OFF:
myInsideFormatRestrictingTag = true;
break;
case NONE:
break;
}
}
private void checkRange(TextRange textRange) {
if (textRange.getLength() == 0) {
ASSERT.assertInvalidRanges(textRange.getStartOffset(), textRange.getEndOffset(), myModel, "empty block");
}
}
private boolean currentWhiteSpaceIsReadOnly() {
if (myCurrentSpaceProperty != null && myCurrentSpaceProperty.isReadOnly()) {
return true;
}
else {
if (myAffectedRanges == null) return false;
return myAffectedRanges.isWhitespaceReadOnly(myCurrentWhiteSpace.getTextRange());
}
}
private boolean isAffectedByFormatting(final Block block) {
if (myAffectedRanges == null) return true;
List<FormatTextRange> allRanges = myAffectedRanges.getRanges();
Document document = myModel.getDocument();
int docLength = document.getTextLength();
for (FormatTextRange range : allRanges) {
int startOffset = range.getStartOffset();
if (startOffset >= docLength) continue;
int lineNumber = document.getLineNumber(startOffset);
int lineEndOffset = document.getLineEndOffset(lineNumber);
int blockStartOffset = block.getTextRange().getStartOffset();
if (blockStartOffset >= startOffset && blockStartOffset < lineEndOffset) {
return true;
}
}
return false;
}
private boolean isInsideFormattingRanges(final Block block) {
if (myAffectedRanges == null) return true;
return !myAffectedRanges.isReadOnly(block.getTextRange());
}
public Map<AbstractBlockWrapper, Block> getBlockToInfoMap() {
return myResult;
}
public LeafBlockWrapper getFirstTokenBlock() {
return myFirstTokenBlock;
}
public LeafBlockWrapper getLastTokenBlock() {
return myLastTokenBlock;
}
public Set<Alignment> getAlignmentsInsideRangeToModify() {
return myAlignmentsInsideRangeToModify;
}
public MultiMap<ExpandableIndent, AbstractBlockWrapper> getExpandableIndentsBlocks() {
return myBlocksToForceChildrenIndent;
}
public MultiMap<Alignment, Block> getBlocksToAlign() {
return myBlocksToAlign;
}
public void setCollectAlignmentsInsideFormattingRange(boolean value) {
myCollectAlignmentsInsideFormattingRange = value;
}
}