| /* |
| * 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.diagnostic.LogMessageEx; |
| import com.intellij.lang.LanguageFormatting; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.codeStyle.CodeStyleSettings; |
| import com.intellij.psi.codeStyle.CommonCodeStyleSettings; |
| import com.intellij.psi.formatter.FormattingDocumentModelImpl; |
| import com.intellij.psi.formatter.ReadOnlyBlockInformationProvider; |
| import com.intellij.psi.impl.DebugUtil; |
| import com.intellij.util.containers.Stack; |
| import gnu.trove.THashMap; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * 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. |
| */ |
| class InitialInfoBuilder { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.formatting.InitialInfoBuilder"); |
| |
| private final Map<AbstractBlockWrapper, Block> myResult = new THashMap<AbstractBlockWrapper, Block>(); |
| |
| private final FormattingDocumentModel myModel; |
| private final FormatTextRanges myAffectedRanges; |
| private final int myPositionOfInterest; |
| @NotNull |
| private final FormattingProgressCallback myProgressCallback; |
| private final FormatterTagHandler myFormatterTagHandler; |
| |
| private final CommonCodeStyleSettings.IndentOptions myOptions; |
| |
| private final Stack<State> myStates = new Stack<State>(); |
| private WhiteSpace myCurrentWhiteSpace; |
| private CompositeBlockWrapper myRootBlockWrapper; |
| private LeafBlockWrapper myPreviousBlock; |
| private LeafBlockWrapper myFirstTokenBlock; |
| private LeafBlockWrapper myLastTokenBlock; |
| private SpacingImpl myCurrentSpaceProperty; |
| private ReadOnlyBlockInformationProvider myReadOnlyBlockInformationProvider; |
| private boolean myReadOnlyMode; |
| |
| private static final boolean INLINE_TABS_ENABLED = "true".equalsIgnoreCase(System.getProperty("inline.tabs.enabled")); |
| |
| private InitialInfoBuilder(final FormattingDocumentModel model, |
| @Nullable final FormatTextRanges affectedRanges, |
| @NotNull CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions options, |
| final int positionOfInterest, |
| @NotNull FormattingProgressCallback progressCallback) |
| { |
| myModel = model; |
| myAffectedRanges = affectedRanges; |
| myProgressCallback = progressCallback; |
| myCurrentWhiteSpace = new WhiteSpace(0, true); |
| myOptions = options; |
| myPositionOfInterest = positionOfInterest; |
| myReadOnlyMode = false; |
| myFormatterTagHandler = new FormatterTagHandler(settings); |
| } |
| |
| public static InitialInfoBuilder prepareToBuildBlocksSequentially(Block root, |
| FormattingDocumentModel model, |
| @Nullable final FormatTextRanges affectedRanges, |
| @NotNull CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions options, |
| int interestingOffset, |
| @NotNull FormattingProgressCallback progressCallback) |
| { |
| InitialInfoBuilder builder = new InitialInfoBuilder(model, affectedRanges, settings, options, interestingOffset, progressCallback); |
| builder.buildFrom(root, 0, null, null, null, true); |
| return builder; |
| } |
| |
| /** |
| * Asks current builder to wrap one more remaining {@link Block code block} (if any). |
| * |
| * @return <code>true</code> if all blocks are wrapped; <code>false</code> otherwise |
| */ |
| public boolean iteration() { |
| if (myStates.isEmpty()) { |
| return true; |
| } |
| |
| State state = myStates.peek(); |
| doIteration(state); |
| return myStates.isEmpty(); |
| } |
| |
| /** |
| * Wraps given root block and all of its descendants and returns root block wrapper. |
| * <p/> |
| * This method performs necessary infrastructure actions and delegates actual processing to |
| * {@link #buildCompositeBlock(Block, CompositeBlockWrapper, int, WrapImpl, boolean)} and |
| * {@link #processSimpleBlock(Block, CompositeBlockWrapper, boolean, int, Block)}. |
| * |
| * @param rootBlock block to wrap |
| * @param index index of the current block at its parent block. <code>-1</code> may be used here if we don't |
| * have information about parent block |
| * @param parent parent block wrapper. <code>null</code> may be used here we no parent block wrapper exists |
| * @param currentWrapParent parent wrap if any; <code>null</code> otherwise |
| * @param parentBlock parent block of the block to wrap |
| * @param rootBlockIsRightBlock flag that shows if target block is the right-most block |
| * @return wrapper for the given <code>'rootBlock'</code> |
| */ |
| private AbstractBlockWrapper buildFrom(final Block rootBlock, |
| final int index, |
| @Nullable final CompositeBlockWrapper parent, |
| @Nullable WrapImpl currentWrapParent, |
| @Nullable final Block parentBlock, |
| boolean rootBlockIsRightBlock) |
| { |
| 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) { |
| if (textRange.getStartOffset() < parent.getStartOffset()) { |
| assertInvalidRanges( |
| textRange.getStartOffset(), |
| parent.getStartOffset(), |
| myModel, |
| "child block start is less than parent block start" |
| ); |
| } |
| |
| if (textRange.getEndOffset() > parent.getEndOffset()) { |
| assertInvalidRanges( |
| textRange.getEndOffset(), |
| parent.getEndOffset(), |
| myModel, |
| "child block end is after parent block end" |
| ); |
| } |
| } |
| |
| myCurrentWhiteSpace.append(blockStartOffset, myModel, myOptions); |
| boolean isReadOnly = isReadOnly(rootBlock, rootBlockIsRightBlock); |
| |
| ReadOnlyBlockInformationProvider previousProvider = myReadOnlyBlockInformationProvider; |
| try { |
| if (rootBlock instanceof ReadOnlyBlockInformationProvider) { |
| myReadOnlyBlockInformationProvider = (ReadOnlyBlockInformationProvider)rootBlock; |
| } |
| if (isReadOnly) { |
| return processSimpleBlock(rootBlock, parent, true, index, parentBlock); |
| } |
| |
| final List<Block> subBlocks = rootBlock.getSubBlocks(); |
| if (subBlocks.isEmpty() || myReadOnlyBlockInformationProvider != null |
| && myReadOnlyBlockInformationProvider.isReadOnly(rootBlock)) { |
| final AbstractBlockWrapper wrapper = processSimpleBlock(rootBlock, parent, false, index, parentBlock); |
| if (!subBlocks.isEmpty()) { |
| wrapper.setIndent((IndentImpl)subBlocks.get(0).getIndent()); |
| } |
| return wrapper; |
| } |
| return buildCompositeBlock(rootBlock, parent, index, currentWrapParent, rootBlockIsRightBlock); |
| } |
| finally { |
| myReadOnlyBlockInformationProvider = previousProvider; |
| } |
| } |
| |
| private CompositeBlockWrapper buildCompositeBlock(final Block rootBlock, |
| @Nullable final CompositeBlockWrapper parent, |
| final int index, |
| @Nullable final WrapImpl currentWrapParent, |
| boolean rootBlockIsRightBlock) |
| { |
| 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; |
| |
| State state = new State(rootBlock, wrappedRootBlock, currentWrapParent, blocksAreReadOnly, rootBlockIsRightBlock); |
| myStates.push(state); |
| return wrappedRootBlock; |
| } |
| |
| private void doIteration(@NotNull State state) { |
| List<Block> subBlocks = state.parentBlock.getSubBlocks(); |
| final int subBlocksCount = subBlocks.size(); |
| int childBlockIndex = state.getIndexOfChildBlockToProcess(); |
| final Block block = subBlocks.get(childBlockIndex); |
| if (state.previousBlock != null || (myCurrentWhiteSpace != null && myCurrentWhiteSpace.isIsFirstWhiteSpace())) { |
| myCurrentSpaceProperty = (SpacingImpl)state.parentBlock.getSpacing(state.previousBlock, block); |
| } |
| |
| boolean childBlockIsRightBlock = false; |
| |
| if (childBlockIndex == subBlocksCount - 1 && state.parentBlockIsRightBlock) { |
| childBlockIsRightBlock = true; |
| } |
| |
| final AbstractBlockWrapper wrapper = buildFrom( |
| block, childBlockIndex, state.wrappedBlock, state.parentBlockWrap, state.parentBlock, childBlockIsRightBlock |
| ); |
| |
| if (wrapper.getIndent() == null) { |
| wrapper.setIndent((IndentImpl)block.getIndent()); |
| } |
| if (!state.readOnly) { |
| try { |
| subBlocks.set(childBlockIndex, null); // to prevent extra strong refs during model building |
| } catch (Throwable ex) { |
| // read-only blocks |
| } |
| } |
| |
| if (state.childBlockProcessed(block, wrapper)) { |
| while (!myStates.isEmpty() && myStates.peek().isProcessed()) { |
| myStates.pop(); |
| } |
| } |
| } |
| |
| private void setDefaultIndents(final List<AbstractBlockWrapper> list) { |
| if (!list.isEmpty()) { |
| for (AbstractBlockWrapper wrapper : list) { |
| if (wrapper.getIndent() == null) { |
| wrapper.setIndent((IndentImpl)Indent.getContinuationWithoutFirstIndent(myOptions.USE_RELATIVE_INDENTS)); |
| } |
| } |
| } |
| } |
| |
| private AbstractBlockWrapper processSimpleBlock(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); |
| } |
| final LeafBlockWrapper info = |
| new LeafBlockWrapper(rootBlock, parent, myCurrentWhiteSpace, myModel, myOptions, myPreviousBlock, readOnly); |
| if (index == 0) { |
| info.arrangeParentTextRange(); |
| } |
| |
| switch (myFormatterTagHandler.getFormatterTag(rootBlock)) { |
| case ON: |
| myReadOnlyMode = false; |
| break; |
| case OFF: |
| myReadOnlyMode = true; |
| break; |
| case NONE: |
| break; |
| } |
| |
| TextRange textRange = rootBlock.getTextRange(); |
| if (textRange.getLength() == 0) { |
| assertInvalidRanges( |
| textRange.getStartOffset(), |
| textRange.getEndOffset(), |
| myModel, |
| "empty block" |
| ); |
| } |
| 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()); |
| } |
| |
| info.setSpaceProperty(myCurrentSpaceProperty); |
| myCurrentWhiteSpace = new WhiteSpace(textRange.getEndOffset(), false); |
| if (myReadOnlyMode) 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 boolean currentWhiteSpaceIsReadOnly() { |
| if (myCurrentSpaceProperty != null && myCurrentSpaceProperty.isReadOnly()) { |
| return true; |
| } |
| else { |
| if (myAffectedRanges == null) return false; |
| return myAffectedRanges.isWhitespaceReadOnly(myCurrentWhiteSpace.getTextRange()); |
| } |
| } |
| |
| private boolean isReadOnly(final Block block, boolean rootIsRightBlock) { |
| if (myAffectedRanges == null) return false; |
| return myAffectedRanges.isReadOnly(block.getTextRange(), rootIsRightBlock); |
| } |
| |
| public Map<AbstractBlockWrapper, Block> getBlockToInfoMap() { |
| return myResult; |
| } |
| |
| public CompositeBlockWrapper getRootBlockWrapper() { |
| return myRootBlockWrapper; |
| } |
| |
| public LeafBlockWrapper getFirstTokenBlock() { |
| return myFirstTokenBlock; |
| } |
| |
| public LeafBlockWrapper getLastTokenBlock() { |
| return myLastTokenBlock; |
| } |
| |
| public static void assertInvalidRanges(final int startOffset, final int newEndOffset, FormattingDocumentModel model, String message) { |
| @NonNls final StringBuilder buffer = new StringBuilder(); |
| buffer.append("Invalid formatting blocks:").append(message).append("\n"); |
| buffer.append("Start offset:"); |
| buffer.append(startOffset); |
| buffer.append(" end offset:"); |
| buffer.append(newEndOffset); |
| buffer.append("\n"); |
| |
| int minOffset = Math.max(Math.min(startOffset, newEndOffset) - 20, 0); |
| int maxOffset = Math.min(Math.max(startOffset, newEndOffset) + 20, model.getTextLength()); |
| |
| buffer.append("Affected text fragment:[").append(minOffset).append(",").append(maxOffset).append("] - '") |
| .append(model.getText(new TextRange(minOffset, maxOffset))).append("'\n"); |
| |
| final StringBuilder messageBuffer = new StringBuilder(); |
| messageBuffer.append("Invalid ranges during formatting"); |
| if (model instanceof FormattingDocumentModelImpl) { |
| messageBuffer.append(" in ").append(((FormattingDocumentModelImpl)model).getFile().getLanguage()); |
| } |
| |
| buffer.append("File text:(").append(model.getTextLength()).append(")\n'"); |
| buffer.append(model.getText(new TextRange(0, model.getTextLength())).toString()); |
| buffer.append("'\n"); |
| buffer.append("model (").append(model.getClass()).append("): ").append(model); |
| |
| Throwable currentThrowable = new Throwable(); |
| if (model instanceof FormattingDocumentModelImpl) { |
| final FormattingDocumentModelImpl modelImpl = (FormattingDocumentModelImpl)model; |
| buffer.append("Psi Tree:\n"); |
| final PsiFile file = modelImpl.getFile(); |
| final List<PsiFile> roots = file.getViewProvider().getAllFiles(); |
| for (PsiFile root : roots) { |
| buffer.append("Root "); |
| DebugUtil.treeToBuffer(buffer, root.getNode(), 0, false, true, true, true); |
| } |
| buffer.append('\n'); |
| currentThrowable = makeLanguageStackTrace(currentThrowable, file); |
| } |
| |
| LogMessageEx.error(LOG, messageBuffer.toString(), currentThrowable, buffer.toString()); |
| } |
| |
| private static Throwable makeLanguageStackTrace(@NotNull Throwable currentThrowable, @NotNull PsiFile file) { |
| Throwable langThrowable = new Throwable(); |
| FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(file); |
| if (builder == null) return currentThrowable; |
| Class builderClass = builder.getClass(); |
| Class declaringClass = builderClass.getDeclaringClass(); |
| String guessedFileName = (declaringClass == null ? builderClass.getSimpleName() : declaringClass.getSimpleName()) + ".java"; |
| StackTraceElement ste = new StackTraceElement(builder.getClass().getName(), "createModel", guessedFileName, 1); |
| StackTraceElement[] originalStackTrace = currentThrowable.getStackTrace(); |
| StackTraceElement[] modifiedStackTrace = new StackTraceElement[originalStackTrace.length + 1]; |
| System.arraycopy(originalStackTrace, 0, modifiedStackTrace, 1, originalStackTrace.length); |
| modifiedStackTrace[0] = ste; |
| langThrowable.setStackTrace(modifiedStackTrace); |
| return langThrowable; |
| } |
| |
| /** |
| * We want to wrap {@link Block code blocks} sequentially, hence, need to store a processing state and continue from the point |
| * where we stopped the processing last time. |
| * <p/> |
| * Current class defines common contract for the state required for such a processing. |
| */ |
| private class State { |
| |
| public final Block parentBlock; |
| public final WrapImpl parentBlockWrap; |
| public final CompositeBlockWrapper wrappedBlock; |
| public final boolean readOnly; |
| public final boolean parentBlockIsRightBlock; |
| |
| public Block previousBlock; |
| |
| private final List<AbstractBlockWrapper> myWrappedChildren = new ArrayList<AbstractBlockWrapper>(); |
| |
| State(@NotNull Block parentBlock, @NotNull CompositeBlockWrapper wrappedBlock, @Nullable WrapImpl parentBlockWrap, |
| boolean readOnly, boolean parentBlockIsRightBlock) |
| { |
| this.parentBlock = parentBlock; |
| this.wrappedBlock = wrappedBlock; |
| this.parentBlockWrap = parentBlockWrap; |
| this.readOnly = readOnly; |
| this.parentBlockIsRightBlock = parentBlockIsRightBlock; |
| } |
| |
| /** |
| * @return index of the first non-processed {@link Block#getSubBlocks() child block} of the {@link #parentBlock target block} |
| */ |
| public int getIndexOfChildBlockToProcess() { |
| return myWrappedChildren.size(); |
| } |
| |
| /** |
| * Notifies current state that child block is processed. |
| * |
| * @return <code>true</code> if all child blocks of the block denoted by the current state are processed; |
| * <code>false</code> otherwise |
| */ |
| public boolean childBlockProcessed(@NotNull Block child, @NotNull AbstractBlockWrapper wrappedChild) { |
| myWrappedChildren.add(wrappedChild); |
| previousBlock = child; |
| |
| int subBlocksNumber = parentBlock.getSubBlocks().size(); |
| if (myWrappedChildren.size() > subBlocksNumber) { |
| return true; |
| } |
| else if (myWrappedChildren.size() == subBlocksNumber) { |
| setDefaultIndents(myWrappedChildren); |
| wrappedBlock.setChildren(myWrappedChildren); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * @return <code>true</code> if current state is processed (basically, if all {@link Block#getSubBlocks() child blocks}) |
| * of the {@link #parentBlock target block} are processed; <code>false</code> otherwise |
| */ |
| public boolean isProcessed() { |
| return myWrappedChildren.size() == parentBlock.getSubBlocks().size(); |
| } |
| } |
| } |