/* * 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.lang.ASTNode; import com.intellij.lang.Language; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.TextChange; import com.intellij.openapi.editor.ex.DocumentEx; import com.intellij.openapi.editor.impl.BulkChangesMerger; import com.intellij.openapi.editor.impl.TextChangeImpl; import com.intellij.openapi.fileTypes.StdFileTypes; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import com.intellij.util.ui.UIUtil; import gnu.trove.TIntObjectHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; class FormatProcessor { private static final Map ALIGNMENT_PROCESSORS = new EnumMap(Alignment.Anchor.class); static { ALIGNMENT_PROCESSORS.put(Alignment.Anchor.LEFT, new LeftEdgeAlignmentProcessor()); ALIGNMENT_PROCESSORS.put(Alignment.Anchor.RIGHT, new RightEdgeAlignmentProcessor()); } /** * There is a possible case that formatting introduced big number of changes to the underlying document. That number may be * big enough for that their subsequent appliance is much slower than direct replacing of the whole document text. *

* Current constant holds minimum number of changes that should trigger such 'replace whole text' optimization. */ private static final int BULK_REPLACE_OPTIMIZATION_CRITERIA = 3000; private static final Logger LOG = Logger.getInstance("#com.intellij.formatting.FormatProcessor"); private LeafBlockWrapper myCurrentBlock; private Map myInfos; private CompositeBlockWrapper myRootBlockWrapper; private TIntObjectHashMap myTextRangeToWrapper; private final CommonCodeStyleSettings.IndentOptions myDefaultIndentOption; private final CodeStyleSettings mySettings; private final Document myDocument; /** * Remembers mappings between backward-shifted aligned block and blocks that cause that shift in order to detect * infinite cycles that may occur when, for example following alignment is specified: *

*

   *     int i1     = 1;
   *     int i2, i3 = 2;
   * 
*

* There is a possible case that 'i1', 'i2' and 'i3' blocks re-use * the same alignment, hence, 'i1' is shifted to right during 'i3' processing but * that causes 'i2' to be shifted right as wll because it's aligned to 'i1' that * increases offset of 'i3' that, in turn, causes backward shift of 'i1' etc. *

* This map remembers such backward shifts in order to be able to break such infinite cycles. */ private final Map> myBackwardShiftedAlignedBlocks = new HashMap>(); private final Map> myAlignmentMappings = new HashMap>(); /** * There is a possible case that we detect a 'cycled alignment' rules (see {@link #myBackwardShiftedAlignedBlocks}). We want * just to skip processing for such alignments then. *

* This container holds 'bad alignment' objects that should not be processed. */ private final Set myAlignmentsToSkip = new HashSet(); private LeafBlockWrapper myWrapCandidate = null; private LeafBlockWrapper myFirstWrappedBlockOnLine = null; private LeafBlockWrapper myFirstTokenBlock; private LeafBlockWrapper myLastTokenBlock; /** * Formatter provides a notion of {@link DependantSpacingImpl dependent spacing}, i.e. spacing that insist on line feed if target * dependent region contains line feed. *

* Example: *

   *       int[] data = {1, 2, 3};
   * 
* We want to keep that in one line if possible but place curly braces on separate lines if the width is not enough: *
   *      int[] data = {    | < right margin
   *          1, 2, 3       |
   *      }                 |
   * 
* There is a possible case that particular block has dependent spacing property that targets region that lays beyond the * current block. E.g. consider example above - '1' block has dependent spacing that targets the whole * '{1, 2, 3}' block. So, it's not possible to answer whether line feed should be used during processing block * '1'. *

* We store such 'forward dependencies' at the current collection where the key is the range of the target 'dependent forward * region' and value is dependent spacing object. *

* Every time we detect that formatter changes 'has line feeds' status of such dependent region, we * {@link DependantSpacingImpl#setDependentRegionChanged() mark} the dependent spacing as changed and schedule one more * formatting iteration. */ private SortedMap myPreviousDependencies = new TreeMap(new Comparator() { @Override public int compare(final TextRange o1, final TextRange o2) { int offsetsDelta = o1.getEndOffset() - o2.getEndOffset(); if (offsetsDelta == 0) { offsetsDelta = o2.getStartOffset() - o1.getStartOffset(); // starting earlier is greater } return offsetsDelta; } }); private final HashSet myAlignAgain = new HashSet(); @NotNull private final FormattingProgressCallback myProgressCallback; private WhiteSpace myLastWhiteSpace; private boolean myDisposed; private CommonCodeStyleSettings.IndentOptions myJavaIndentOptions; private final int myRightMargin; @NotNull private State myCurrentState; public FormatProcessor(final FormattingDocumentModel docModel, Block rootBlock, CodeStyleSettings settings, CommonCodeStyleSettings.IndentOptions indentOptions, @Nullable FormatTextRanges affectedRanges, @NotNull FormattingProgressCallback progressCallback) { this(docModel, rootBlock, settings, indentOptions, affectedRanges, -1, progressCallback); } public FormatProcessor(final FormattingDocumentModel docModel, Block rootBlock, CodeStyleSettings settings, CommonCodeStyleSettings.IndentOptions indentOptions, @Nullable FormatTextRanges affectedRanges, int interestingOffset, @NotNull FormattingProgressCallback progressCallback) { myProgressCallback = progressCallback; myDefaultIndentOption = indentOptions; mySettings = settings; myDocument = docModel.getDocument(); myCurrentState = new WrapBlocksState(rootBlock, docModel, affectedRanges, interestingOffset); myRightMargin = getRightMargin(rootBlock); } private int getRightMargin(Block rootBlock) { Language language = null; if (rootBlock instanceof ASTBlock) { ASTNode node = ((ASTBlock)rootBlock).getNode(); if (node != null) { PsiElement psiElement = node.getPsi(); if (psiElement.isValid()) { PsiFile psiFile = psiElement.getContainingFile(); if (psiFile != null) { language = psiFile.getViewProvider().getBaseLanguage(); } } } } return mySettings.getRightMargin(language); } private LeafBlockWrapper getLastBlock() { LeafBlockWrapper result = myFirstTokenBlock; while (result.getNextBlock() != null) { result = result.getNextBlock(); } return result; } private static TIntObjectHashMap buildTextRangeToInfoMap(final LeafBlockWrapper first) { final TIntObjectHashMap result = new TIntObjectHashMap(); LeafBlockWrapper current = first; while (current != null) { result.put(current.getStartOffset(), current); current = current.getNextBlock(); } return result; } public void format(FormattingModel model) { format(model, false); } /** * Asks current processor to perform formatting. *

* There are two processing approaches at the moment: *

   * 
    *
  • perform formatting during the current method call;
  • *
  • * split the whole formatting process to the set of fine-grained tasks and execute them sequentially during * subsequent {@link #iteration()} calls; *
  • *
*
*

* Here is rationale for the second approach - formatting may introduce changes to the underlying document and IntelliJ IDEA * is designed in a way that write access is allowed from EDT only. That means that every time we execute particular action * from EDT we have no chance of performing any other actions from EDT simultaneously (e.g. we may want to show progress bar * that reflects current formatting state but the progress bar can' bet updated if formatting is performed during a single long * method call). So, we can interleave formatting iterations with GUI state updates. * * @param model target formatting model * @param sequentially flag that indicates what kind of processing should be used */ public void format(FormattingModel model, boolean sequentially) { if (sequentially) { AdjustWhiteSpacesState adjustState = new AdjustWhiteSpacesState(); adjustState.setNext(new ApplyChangesState(model)); myCurrentState.setNext(adjustState); } else { formatWithoutRealModifications(false); performModifications(model, false); } } /** * Asks current processor to perform processing iteration * * @return true if the processing is finished; false otherwise * @see #format(FormattingModel, boolean) */ public boolean iteration() { if (myCurrentState.isDone()) { return true; } myCurrentState.iteration(); return myCurrentState.isDone(); } /** * Asks current processor to stop any active sequential processing if any. */ public void stopSequentialProcessing() { myCurrentState.stop(); } public void formatWithoutRealModifications() { formatWithoutRealModifications(false); } @SuppressWarnings({"WhileLoopSpinsOnField"}) public void formatWithoutRealModifications(boolean sequentially) { myCurrentState.setNext(new AdjustWhiteSpacesState()); if (sequentially) { return; } doIterationsSynchronously(FormattingStateId.PROCESSING_BLOCKS); } private void reset() { myBackwardShiftedAlignedBlocks.clear(); myAlignmentMappings.clear(); myPreviousDependencies.clear(); myWrapCandidate = null; if (myRootBlockWrapper != null) { myRootBlockWrapper.reset(); } } public void performModifications(FormattingModel model) { performModifications(model, false); } public void performModifications(FormattingModel model, boolean sequentially) { assert !myDisposed; myCurrentState.setNext(new ApplyChangesState(model)); if (sequentially) { return; } doIterationsSynchronously(FormattingStateId.APPLYING_CHANGES); } /** * Perform iterations against the {@link #myCurrentState current state} until it's {@link FormattingStateId type} * is {@link FormattingStateId#getPreviousStates() less} or equal to the given state. * * @param state target state to process */ private void doIterationsSynchronously(@NotNull FormattingStateId state) { while ((myCurrentState.getStateId() == state || state.getPreviousStates().contains(myCurrentState.getStateId())) && !myCurrentState.isDone()) { myCurrentState.iteration(); } } public void setJavaIndentOptions(final CommonCodeStyleSettings.IndentOptions javaIndentOptions) { myJavaIndentOptions = javaIndentOptions; } /** * Decides whether applying formatter changes should be applied incrementally one-by-one or merge result should be * constructed locally and the whole document text should be replaced. Performs such single bulk change if necessary. * * @param blocksToModify changes introduced by formatter * @param model current formatting model * @param indentOption indent options to use * @return true if given changes are applied to the document (i.e. no further processing is required); * false otherwise */ @SuppressWarnings({"deprecation"}) private boolean applyChangesAtRewriteMode(@NotNull final List blocksToModify, @NotNull final FormattingModel model, @NotNull CommonCodeStyleSettings.IndentOptions indentOption) { FormattingDocumentModel documentModel = model.getDocumentModel(); Document document = documentModel.getDocument(); if (document == null) { return false; } List changes = new ArrayList(); int shift = 0; int currentIterationShift = 0; for (LeafBlockWrapper block : blocksToModify) { WhiteSpace whiteSpace = block.getWhiteSpace(); CharSequence newWs = documentModel.adjustWhiteSpaceIfNecessary( whiteSpace.generateWhiteSpace(getIndentOptionsToUse(block, indentOption)), whiteSpace.getStartOffset(), whiteSpace.getEndOffset(), block.getNode(), false ); if (changes.size() > 10000) { CharSequence mergeResult = BulkChangesMerger.INSTANCE.mergeToCharSequence(document.getChars(), document.getTextLength(), changes); document.replaceString(0, document.getTextLength(), mergeResult); shift += currentIterationShift; currentIterationShift = 0; changes.clear(); } TextChangeImpl change = new TextChangeImpl(newWs, whiteSpace.getStartOffset() + shift, whiteSpace.getEndOffset() + shift); currentIterationShift += change.getDiff(); changes.add(change); } CharSequence mergeResult = BulkChangesMerger.INSTANCE.mergeToCharSequence(document.getChars(), document.getTextLength(), changes); document.replaceString(0, document.getTextLength(), mergeResult); cleanupBlocks(blocksToModify); return true; } private static void cleanupBlocks(List blocks) { for (LeafBlockWrapper block : blocks) { block.getParent().dispose(); block.dispose(); } blocks.clear(); } @Nullable private static DocumentEx getAffectedDocument(final FormattingModel model) { final Document document = model.getDocumentModel().getDocument(); if (document instanceof DocumentEx) { return (DocumentEx)document; } else { return null; } } private static int replaceWhiteSpace(final FormattingModel model, @NotNull final LeafBlockWrapper block, int shift, final CharSequence _newWhiteSpace, final CommonCodeStyleSettings.IndentOptions options ) { final WhiteSpace whiteSpace = block.getWhiteSpace(); final TextRange textRange = whiteSpace.getTextRange(); final TextRange wsRange = shiftRange(textRange, shift); final String newWhiteSpace = _newWhiteSpace.toString(); TextRange newWhiteSpaceRange = model instanceof FormattingModelEx ? ((FormattingModelEx) model).replaceWhiteSpace(wsRange, block.getNode(), newWhiteSpace) : model.replaceWhiteSpace(wsRange, newWhiteSpace); shift += newWhiteSpaceRange.getLength() - textRange.getLength(); if (block.isLeaf() && whiteSpace.containsLineFeeds() && block.containsLineFeeds()) { final TextRange currentBlockRange = shiftRange(block.getTextRange(), shift); IndentInside oldBlockIndent = whiteSpace.getInitialLastLineIndent(); IndentInside whiteSpaceIndent = IndentInside.createIndentOn(IndentInside.getLastLine(newWhiteSpace)); final int shiftInside = calcShift(oldBlockIndent, whiteSpaceIndent, options); if (shiftInside != 0 || !oldBlockIndent.equals(whiteSpaceIndent)) { final TextRange newBlockRange = model.shiftIndentInsideRange(currentBlockRange, shiftInside); shift += newBlockRange.getLength() - block.getLength(); } } return shift; } @NotNull private List collectBlocksToModify() { List blocksToModify = new ArrayList(); for (LeafBlockWrapper block = myFirstTokenBlock; block != null; block = block.getNextBlock()) { final WhiteSpace whiteSpace = block.getWhiteSpace(); if (!whiteSpace.isReadOnly()) { final String newWhiteSpace = whiteSpace.generateWhiteSpace(getIndentOptionsToUse(block, myDefaultIndentOption)); if (!whiteSpace.equalsToString(newWhiteSpace)) { blocksToModify.add(block); } } } return blocksToModify; } @NotNull private CommonCodeStyleSettings.IndentOptions getIndentOptionsToUse(@NotNull AbstractBlockWrapper block, @NotNull CommonCodeStyleSettings.IndentOptions fallbackIndentOptions) { final Language language = block.getLanguage(); if (language == null) { return fallbackIndentOptions; } final CommonCodeStyleSettings commonSettings = mySettings.getCommonSettings(language); if (commonSettings == null) { return fallbackIndentOptions; } final CommonCodeStyleSettings.IndentOptions result = commonSettings.getIndentOptions(); return result == null ? fallbackIndentOptions : result; } private static TextRange shiftRange(final TextRange textRange, final int shift) { return new TextRange(textRange.getStartOffset() + shift, textRange.getEndOffset() + shift); } private void processToken() { final SpacingImpl spaceProperty = myCurrentBlock.getSpaceProperty(); final WhiteSpace whiteSpace = myCurrentBlock.getWhiteSpace(); whiteSpace.arrangeLineFeeds(spaceProperty, this); if (!whiteSpace.containsLineFeeds()) { whiteSpace.arrangeSpaces(spaceProperty); } try { if (processWrap()) { return; } } finally { if (whiteSpace.containsLineFeeds()) { onCurrentLineChanged(); } } if (!adjustIndent()) { return; } defineAlignOffset(myCurrentBlock); if (myCurrentBlock.containsLineFeeds()) { onCurrentLineChanged(); } if (shouldSaveDependency(spaceProperty, whiteSpace)) { saveDependency(spaceProperty); } if (!whiteSpace.isIsReadOnly() && shouldReformatBecauseOfBackwardDependency(whiteSpace.getTextRange())) { myAlignAgain.add(whiteSpace); } else if (!myAlignAgain.isEmpty()) { myAlignAgain.remove(whiteSpace); } myCurrentBlock = myCurrentBlock.getNextBlock(); } private boolean shouldReformatBecauseOfBackwardDependency(TextRange changed) { final SortedMap sortedHeadMap = myPreviousDependencies.tailMap(changed); boolean result = false; for (final Map.Entry entry : sortedHeadMap.entrySet()) { final TextRange textRange = entry.getKey(); if (textRange.contains(changed)) { final DependantSpacingImpl dependentSpacing = entry.getValue(); final boolean containedLineFeeds = dependentSpacing.getMinLineFeeds() > 0; final boolean containsLineFeeds = containsLineFeeds(textRange); if (containedLineFeeds != containsLineFeeds) { dependentSpacing.setDependentRegionChanged(); result = true; } } } return result; } private void saveDependency(final SpacingImpl spaceProperty) { final DependantSpacingImpl dependantSpaceProperty = (DependantSpacingImpl)spaceProperty; final TextRange dependency = dependantSpaceProperty.getDependency(); if (dependantSpaceProperty.isDependentRegionChanged()) { return; } myPreviousDependencies.put(dependency, dependantSpaceProperty); } private static boolean shouldSaveDependency(final SpacingImpl spaceProperty, WhiteSpace whiteSpace) { if (!(spaceProperty instanceof DependantSpacingImpl)) return false; if (whiteSpace.isReadOnly() || whiteSpace.isLineFeedsAreReadOnly()) return false; final TextRange dependency = ((DependantSpacingImpl)spaceProperty).getDependency(); return whiteSpace.getStartOffset() < dependency.getEndOffset(); } /** * Processes the wrap of the current block. * * @return true if we have changed myCurrentBlock and need to restart its processing; false if myCurrentBlock is unchanged and we can * continue processing */ private boolean processWrap() { final SpacingImpl spacing = myCurrentBlock.getSpaceProperty(); final WhiteSpace whiteSpace = myCurrentBlock.getWhiteSpace(); final boolean wrapWasPresent = whiteSpace.containsLineFeeds(); if (wrapWasPresent) { myFirstWrappedBlockOnLine = null; if (!whiteSpace.containsLineFeedsInitially()) { whiteSpace.removeLineFeeds(spacing, this); } } final boolean wrapIsPresent = whiteSpace.containsLineFeeds(); final ArrayList wraps = myCurrentBlock.getWraps(); for (WrapImpl wrap : wraps) { wrap.setWrapOffset(myCurrentBlock.getStartOffset()); } final WrapImpl wrap = getWrapToBeUsed(wraps); if (wrap != null || wrapIsPresent) { if (!wrapIsPresent && !canReplaceWrapCandidate(wrap)) { myCurrentBlock = myWrapCandidate; return true; } 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. myCurrentBlock = wrap.getChopStartBlock(); wrap.setActive(); return true; } if (wrap != null && isChopNeeded(wrap)) { wrap.setActive(); } if (!wrapIsPresent) { whiteSpace.ensureLineFeed(); if (!wrapWasPresent) { if (myFirstWrappedBlockOnLine != null && wrap.isChildOf(myFirstWrappedBlockOnLine.getWrap(), myCurrentBlock)) { wrap.ignoreParentWrap(myFirstWrappedBlockOnLine.getWrap(), myCurrentBlock); myCurrentBlock = myFirstWrappedBlockOnLine; return true; } else { myFirstWrappedBlockOnLine = myCurrentBlock; } } } myWrapCandidate = null; } else { for (final WrapImpl wrap1 : wraps) { if (isCandidateToBeWrapped(wrap1) && canReplaceWrapCandidate(wrap1)) { myWrapCandidate = myCurrentBlock; } if (isChopNeeded(wrap1)) { wrap1.saveChopBlock(myCurrentBlock); } } } if (!whiteSpace.containsLineFeeds() && myWrapCandidate != null && !whiteSpace.isReadOnly() && lineOver()) { myCurrentBlock = myWrapCandidate; return true; } return false; } /** * 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 true if wrap of the {@link #myWrapCandidate} object (if any) may be replaced by the given wrap; * false otherwise */ private boolean canReplaceWrapCandidate(WrapImpl wrap) { 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, myCurrentBlock); } private boolean isCandidateToBeWrapped(final WrapImpl wrap) { return isSuitableInTheCurrentPosition(wrap) && (wrap.getType() == WrapImpl.Type.WRAP_AS_NEEDED || wrap.getType() == WrapImpl.Type.CHOP_IF_NEEDED) && !myCurrentBlock.getWhiteSpace().isReadOnly(); } private void onCurrentLineChanged() { myWrapCandidate = null; } /** * Adjusts indent of the current block. * * @return true if current formatting iteration should be continued; * false otherwise (e.g. if previously processed block is shifted inside this method for example * because of specified alignment options) */ private boolean adjustIndent() { AlignmentImpl alignment = CoreFormatterUtil.getAlignment(myCurrentBlock); WhiteSpace whiteSpace = myCurrentBlock.getWhiteSpace(); if (alignment == null || myAlignmentsToSkip.contains(alignment)) { if (whiteSpace.containsLineFeeds()) { adjustSpacingByIndentOffset(); } else { whiteSpace.arrangeSpaces(myCurrentBlock.getSpaceProperty()); } return true; } BlockAlignmentProcessor alignmentProcessor = ALIGNMENT_PROCESSORS.get(alignment.getAnchor()); if (alignmentProcessor == null) { LOG.error(String.format("Can't find alignment processor for alignment anchor %s", alignment.getAnchor())); return true; } BlockAlignmentProcessor.Context context = new BlockAlignmentProcessor.Context( myDocument, alignment, myCurrentBlock, myAlignmentMappings, myBackwardShiftedAlignedBlocks, getIndentOptionsToUse(myCurrentBlock, myDefaultIndentOption) ); BlockAlignmentProcessor.Result result = alignmentProcessor.applyAlignment(context); final LeafBlockWrapper offsetResponsibleBlock = alignment.getOffsetRespBlockBefore(myCurrentBlock); switch (result) { case TARGET_BLOCK_PROCESSED_NOT_ALIGNED: return true; case TARGET_BLOCK_ALIGNED: storeAlignmentMapping(); return true; case BACKWARD_BLOCK_ALIGNED: if (offsetResponsibleBlock == null) { return true; } Set blocksCausedRealignment = new HashSet(); myBackwardShiftedAlignedBlocks.clear(); myBackwardShiftedAlignedBlocks.put(offsetResponsibleBlock, blocksCausedRealignment); blocksCausedRealignment.add(myCurrentBlock); storeAlignmentMapping(myCurrentBlock, offsetResponsibleBlock); myCurrentBlock = offsetResponsibleBlock.getNextBlock(); onCurrentLineChanged(); return false; case RECURSION_DETECTED: myCurrentBlock = offsetResponsibleBlock; // Fall through to the 'register alignment to skip'. case UNABLE_TO_ALIGN_BACKWARD_BLOCK: myAlignmentsToSkip.add(alignment); return false; default: return true; } } /** * We need to track blocks which white spaces are modified because of alignment rules. *

* This method encapsulates the logic of storing such information. */ private void storeAlignmentMapping() { AlignmentImpl alignment = null; AbstractBlockWrapper block = myCurrentBlock; while (alignment == null && block != null) { alignment = block.getAlignment(); block = block.getParent(); } if (alignment != null) { block = alignment.getOffsetRespBlockBefore(myCurrentBlock); if (block != null) { storeAlignmentMapping(myCurrentBlock, block); } } } private void storeAlignmentMapping(AbstractBlockWrapper block1, AbstractBlockWrapper block2) { doStoreAlignmentMapping(block1, block2); doStoreAlignmentMapping(block2, block1); } private void doStoreAlignmentMapping(AbstractBlockWrapper key, AbstractBlockWrapper value) { Set wrappers = myAlignmentMappings.get(key); if (wrappers == null) { myAlignmentMappings.put(key, wrappers = new HashSet()); } wrappers.add(value); } /** * Applies indent to the white space of {@link #myCurrentBlock currently processed wrapped block}. Both indentation * and alignment options are took into consideration here. */ private void adjustLineIndent() { IndentData alignOffset = getAlignOffset(); if (alignOffset == null) { adjustSpacingByIndentOffset(); } else { myCurrentBlock.getWhiteSpace().setSpaces(alignOffset.getSpaces(), alignOffset.getIndentSpaces()); } } private void adjustSpacingByIndentOffset() { IndentData offset = myCurrentBlock.calculateOffset(getIndentOptionsToUse(myCurrentBlock, myDefaultIndentOption)); myCurrentBlock.getWhiteSpace().setSpaces(offset.getSpaces(), offset.getIndentSpaces()); } private boolean isChopNeeded(final WrapImpl wrap) { return wrap != null && wrap.getType() == WrapImpl.Type.CHOP_IF_NEEDED && isSuitableInTheCurrentPosition(wrap); } private boolean isSuitableInTheCurrentPosition(final WrapImpl wrap) { if (wrap.getWrapOffset() < myCurrentBlock.getStartOffset()) { return true; } if (wrap.isWrapFirstElement()) { return true; } if (wrap.getType() == WrapImpl.Type.WRAP_AS_NEEDED) { return positionAfterWrappingIsSuitable(); } return wrap.getType() == WrapImpl.Type.CHOP_IF_NEEDED && lineOver() && positionAfterWrappingIsSuitable(); } /** * Ensures that offset of the {@link #myCurrentBlock currently processed block} is not increased if we make a wrap on it. * * @return true if it's ok to wrap at the currently processed block; false otherwise */ private boolean positionAfterWrappingIsSuitable() { final WhiteSpace whiteSpace = myCurrentBlock.getWhiteSpace(); if (whiteSpace.containsLineFeeds()) return true; final int spaces = whiteSpace.getSpaces(); int indentSpaces = whiteSpace.getIndentSpaces(); try { final int startColumnNow = CoreFormatterUtil.getStartColumn(myCurrentBlock); whiteSpace.ensureLineFeed(); adjustLineIndent(); final int startColumnAfterWrap = CoreFormatterUtil.getStartColumn(myCurrentBlock); return startColumnNow > startColumnAfterWrap; } finally { whiteSpace.removeLineFeeds(myCurrentBlock.getSpaceProperty(), this); whiteSpace.setSpaces(spaces, indentSpaces); } } @Nullable private WrapImpl getWrapToBeUsed(final ArrayList wraps) { if (wraps.isEmpty()) { return null; } if (myWrapCandidate == myCurrentBlock) return wraps.get(0); for (final WrapImpl wrap : wraps) { if (!isSuitableInTheCurrentPosition(wrap)) 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()) { return wrap; } } } return null; } /** * @return true if {@link #myCurrentBlock currently processed wrapped block} doesn't contain line feeds and * exceeds right margin; false otherwise */ private boolean lineOver() { return !myCurrentBlock.containsLineFeeds() && CoreFormatterUtil.getStartColumn(myCurrentBlock) + myCurrentBlock.getLength() > myRightMargin; } private void defineAlignOffset(final LeafBlockWrapper block) { AbstractBlockWrapper current = myCurrentBlock; while (true) { final AlignmentImpl alignment = current.getAlignment(); if (alignment != null) { alignment.setOffsetRespBlock(block); } current = current.getParent(); if (current == null) return; if (current.getStartOffset() != myCurrentBlock.getStartOffset()) return; } } /** * Tries to get align-implied indent of the current block. * * @return indent of the current block if any; null otherwise */ @Nullable private IndentData getAlignOffset() { AbstractBlockWrapper current = myCurrentBlock; while (true) { final AlignmentImpl alignment = current.getAlignment(); LeafBlockWrapper offsetResponsibleBlock; if (alignment != null && (offsetResponsibleBlock = alignment.getOffsetRespBlockBefore(myCurrentBlock)) != null) { final WhiteSpace whiteSpace = offsetResponsibleBlock.getWhiteSpace(); if (whiteSpace.containsLineFeeds()) { return new IndentData(whiteSpace.getIndentSpaces(), whiteSpace.getSpaces()); } else { final int offsetBeforeBlock = CoreFormatterUtil.getStartColumn(offsetResponsibleBlock); final AbstractBlockWrapper indentedParentBlock = CoreFormatterUtil.getIndentedParentBlock(myCurrentBlock); if (indentedParentBlock == null) { return new IndentData(0, offsetBeforeBlock); } else { final int parentIndent = indentedParentBlock.getWhiteSpace().getIndentOffset(); if (parentIndent > offsetBeforeBlock) { return new IndentData(0, offsetBeforeBlock); } else { return new IndentData(parentIndent, offsetBeforeBlock - parentIndent); } } } } else { current = current.getParent(); if (current == null || current.getStartOffset() != myCurrentBlock.getStartOffset()) return null; } } } public boolean containsLineFeeds(final TextRange dependency) { LeafBlockWrapper child = myTextRangeToWrapper.get(dependency.getStartOffset()); if (child == null) return false; if (child.containsLineFeeds()) return true; final int endOffset = dependency.getEndOffset(); while (child.getEndOffset() < endOffset) { child = child.getNextBlock(); if (child == null) return false; if (child.getWhiteSpace().containsLineFeeds()) return true; if (child.containsLineFeeds()) return true; } return false; } @Nullable public LeafBlockWrapper getBlockAfter(final int startOffset) { int current = startOffset; LeafBlockWrapper result = null; while (current < myLastWhiteSpace.getStartOffset()) { final LeafBlockWrapper currentValue = myTextRangeToWrapper.get(current); if (currentValue != null) { result = currentValue; break; } current++; } LeafBlockWrapper prevBlock = getPrevBlock(result); if (prevBlock != null && prevBlock.contains(startOffset)) { return prevBlock; } else { return result; } } @Nullable private LeafBlockWrapper getPrevBlock(@Nullable final LeafBlockWrapper result) { if (result != null) { return result.getPreviousBlock(); } else { return myLastTokenBlock; } } public void setAllWhiteSpacesAreReadOnly() { LeafBlockWrapper current = myFirstTokenBlock; while (current != null) { current.getWhiteSpace().setReadOnly(true); current = current.getNextBlock(); } } static class ChildAttributesInfo { public final AbstractBlockWrapper parent; final ChildAttributes attributes; final int index; public ChildAttributesInfo(final AbstractBlockWrapper parent, final ChildAttributes attributes, final int index) { this.parent = parent; this.attributes = attributes; this.index = index; } } public IndentInfo getIndentAt(final int offset) { processBlocksBefore(offset); AbstractBlockWrapper parent = getParentFor(offset, myCurrentBlock); if (parent == null) { final LeafBlockWrapper previousBlock = myCurrentBlock.getPreviousBlock(); if (previousBlock != null) parent = getParentFor(offset, previousBlock); if (parent == null) return new IndentInfo(0, 0, 0); } int index = getNewChildPosition(parent, offset); final Block block = myInfos.get(parent); if (block == null) { return new IndentInfo(0, 0, 0); } ChildAttributesInfo info = getChildAttributesInfo(block, index, parent); if (info == null) { return new IndentInfo(0, 0, 0); } return adjustLineIndent(info.parent, info.attributes, info.index); } @Nullable private static ChildAttributesInfo getChildAttributesInfo(@NotNull final Block block, final int index, @Nullable AbstractBlockWrapper parent) { if (parent == null) { return null; } ChildAttributes childAttributes = block.getChildAttributes(index); if (childAttributes == ChildAttributes.DELEGATE_TO_PREV_CHILD) { final Block newBlock = block.getSubBlocks().get(index - 1); AbstractBlockWrapper prevWrappedBlock; if (parent instanceof CompositeBlockWrapper) { prevWrappedBlock = ((CompositeBlockWrapper)parent).getChildren().get(index - 1); } else { prevWrappedBlock = parent.getPreviousBlock(); } return getChildAttributesInfo(newBlock, newBlock.getSubBlocks().size(), prevWrappedBlock); } else if (childAttributes == ChildAttributes.DELEGATE_TO_NEXT_CHILD) { AbstractBlockWrapper nextWrappedBlock; if (parent instanceof CompositeBlockWrapper) { List children = ((CompositeBlockWrapper)parent).getChildren(); if (children != null && index < children.size()) { nextWrappedBlock = children.get(index); } else { return null; } } else { nextWrappedBlock = ((LeafBlockWrapper)parent).getNextBlock(); } return getChildAttributesInfo(block.getSubBlocks().get(index), 0, nextWrappedBlock); } else { return new ChildAttributesInfo(parent, childAttributes, index); } } private IndentInfo adjustLineIndent(final AbstractBlockWrapper parent, final ChildAttributes childAttributes, final int index) { int alignOffset = getAlignOffsetBefore(childAttributes.getAlignment(), null); if (alignOffset == -1) { return parent.calculateChildOffset(getIndentOptionsToUse(parent, myDefaultIndentOption), childAttributes, index).createIndentInfo(); } else { AbstractBlockWrapper indentedParentBlock = CoreFormatterUtil.getIndentedParentBlock(myCurrentBlock); if (indentedParentBlock == null) { return new IndentInfo(0, 0, alignOffset); } else { int indentOffset = indentedParentBlock.getWhiteSpace().getIndentOffset(); if (indentOffset > alignOffset) { return new IndentInfo(0, 0, alignOffset); } else { return new IndentInfo(0, indentOffset, alignOffset - indentOffset); } } } } private static int getAlignOffsetBefore(@Nullable final Alignment alignment, @Nullable final LeafBlockWrapper blockAfter) { if (alignment == null) return -1; final LeafBlockWrapper alignRespBlock = ((AlignmentImpl)alignment).getOffsetRespBlockBefore(blockAfter); if (alignRespBlock != null) { return CoreFormatterUtil.getStartColumn(alignRespBlock); } else { return -1; } } private static int getNewChildPosition(final AbstractBlockWrapper parent, final int offset) { AbstractBlockWrapper parentBlockToUse = getLastNestedCompositeBlockForSameRange(parent); if (!(parentBlockToUse instanceof CompositeBlockWrapper)) return 0; final List subBlocks = ((CompositeBlockWrapper)parentBlockToUse).getChildren(); //noinspection ConstantConditions if (subBlocks != null) { for (int i = 0; i < subBlocks.size(); i++) { AbstractBlockWrapper block = subBlocks.get(i); if (block.getStartOffset() >= offset) return i; } return subBlocks.size(); } else { return 0; } } @Nullable private static AbstractBlockWrapper getParentFor(final int offset, AbstractBlockWrapper block) { AbstractBlockWrapper current = block; while (current != null) { if (current.getStartOffset() < offset && current.getEndOffset() >= offset) { return current; } current = current.getParent(); } return null; } @Nullable private AbstractBlockWrapper getParentFor(final int offset, LeafBlockWrapper block) { AbstractBlockWrapper previous = getPreviousIncompleteBlock(block, offset); if (previous != null) { return getLastNestedCompositeBlockForSameRange(previous); } else { return getParentFor(offset, (AbstractBlockWrapper)block); } } @Nullable private AbstractBlockWrapper getPreviousIncompleteBlock(final LeafBlockWrapper block, final int offset) { if (block == null) { if (myLastTokenBlock.isIncomplete()) { return myLastTokenBlock; } else { return null; } } AbstractBlockWrapper current = block; while (current.getParent() != null && current.getParent().getStartOffset() > offset) { current = current.getParent(); } if (current.getParent() == null) return null; if (current.getEndOffset() <= offset) { while (!current.isIncomplete() && current.getParent() != null && current.getParent().getEndOffset() <= offset) { current = current.getParent(); } if (current.isIncomplete()) return current; } if (current.getParent() == null) return null; final List subBlocks = current.getParent().getChildren(); final int index = subBlocks.indexOf(current); if (index < 0) { LOG.assertTrue(false); } if (index == 0) return null; AbstractBlockWrapper currentResult = subBlocks.get(index - 1); if (!currentResult.isIncomplete()) return null; AbstractBlockWrapper lastChild = getLastChildOf(currentResult); while (lastChild != null && lastChild.isIncomplete()) { currentResult = lastChild; lastChild = getLastChildOf(currentResult); } return currentResult; } @Nullable private static AbstractBlockWrapper getLastChildOf(final AbstractBlockWrapper currentResult) { AbstractBlockWrapper parentBlockToUse = getLastNestedCompositeBlockForSameRange(currentResult); if (!(parentBlockToUse instanceof CompositeBlockWrapper)) return null; final List subBlocks = ((CompositeBlockWrapper)parentBlockToUse).getChildren(); if (subBlocks.isEmpty()) return null; return subBlocks.get(subBlocks.size() - 1); } /** * There is a possible case that particular block is a composite block that contains number of nested composite blocks * that all target the same text range. This method allows to derive the most nested block that shares the same range (if any). * * @param block block to check * @return the most nested block of the given one that shares the same text range if any; given block otherwise */ @NotNull private static AbstractBlockWrapper getLastNestedCompositeBlockForSameRange(@NotNull final AbstractBlockWrapper block) { if (!(block instanceof CompositeBlockWrapper)) { return block; } AbstractBlockWrapper result = block; AbstractBlockWrapper candidate = block; while (true) { List subBlocks = ((CompositeBlockWrapper)candidate).getChildren(); if (subBlocks == null || subBlocks.size() != 1) { break; } candidate = subBlocks.get(0); if (candidate.getStartOffset() == block.getStartOffset() && candidate.getEndOffset() == block.getEndOffset() && candidate instanceof CompositeBlockWrapper) { result = candidate; } else { break; } } return result; } private void processBlocksBefore(final int offset) { while (true) { myAlignAgain.clear(); myCurrentBlock = myFirstTokenBlock; while (myCurrentBlock != null && myCurrentBlock.getStartOffset() < offset) { processToken(); if (myCurrentBlock == null) { myCurrentBlock = myLastTokenBlock; if (myCurrentBlock != null) { myProgressCallback.afterProcessingBlock(myCurrentBlock); } break; } } if (myAlignAgain.isEmpty()) return; reset(); } } public LeafBlockWrapper getFirstTokenBlock() { return myFirstTokenBlock; } public WhiteSpace getLastWhiteSpace() { return myLastWhiteSpace; } /** * Calculates difference in visual columns between the given indents. * * @param oldIndent old indent * @param newIndent new indent * @param options indent options to use * @return difference in visual columns between the given indents */ private static int calcShift(@NotNull final IndentInside oldIndent, @NotNull final IndentInside newIndent, @NotNull final CommonCodeStyleSettings.IndentOptions options) { if (oldIndent.equals(newIndent)) return 0; if (options.USE_TAB_CHARACTER) { return (newIndent.tabs - oldIndent.getTabsCount(options)) * options.TAB_SIZE; } else { return newIndent.whiteSpaces - oldIndent.getSpacesCount(options); } } /** * Utility method to use during debugging formatter processing. * * @return text that contains intermediate formatter-introduced changes (even not committed yet) */ @SuppressWarnings("UnusedDeclaration") @NotNull private String getCurrentText() { StringBuilder result = new StringBuilder(); for (LeafBlockWrapper block = myFirstTokenBlock; block != null; block = block.getNextBlock()) { result.append(block.getWhiteSpace().generateWhiteSpace(getIndentOptionsToUse(block, myDefaultIndentOption))); result.append(myDocument.getCharsSequence().subSequence(block.getStartOffset(), block.getEndOffset())); } return result.toString(); } private abstract class State { private final FormattingStateId myStateId; private State myNextState; private boolean myDone; protected State(FormattingStateId stateId) { myStateId = stateId; } public void iteration() { if (!isDone()) { doIteration(); } shiftStateIfNecessary(); } public boolean isDone() { return myDone; } protected void setDone(boolean done) { myDone = done; } public void setNext(@NotNull State state) { if (getStateId() == state.getStateId() || (myNextState != null && myNextState.getStateId() == state.getStateId())) { return; } myNextState = state; shiftStateIfNecessary(); } public FormattingStateId getStateId() { return myStateId; } public void stop() { } protected abstract void doIteration(); protected abstract void prepare(); private void shiftStateIfNecessary() { if (isDone() && myNextState != null) { myCurrentState = myNextState; myNextState = null; myCurrentState.prepare(); } } } private class WrapBlocksState extends State { private final InitialInfoBuilder myWrapper; private final FormattingDocumentModel myModel; WrapBlocksState(@NotNull Block root, @NotNull FormattingDocumentModel model, @Nullable final FormatTextRanges affectedRanges, int interestingOffset) { super(FormattingStateId.WRAPPING_BLOCKS); myModel = model; myWrapper = InitialInfoBuilder.prepareToBuildBlocksSequentially( root, model, affectedRanges, mySettings, myDefaultIndentOption, interestingOffset, myProgressCallback ); } @Override protected void prepare() { } @Override public void doIteration() { if (isDone()) { return; } setDone(myWrapper.iteration()); if (!isDone()) { return; } myInfos = myWrapper.getBlockToInfoMap(); myRootBlockWrapper = myWrapper.getRootBlockWrapper(); myFirstTokenBlock = myWrapper.getFirstTokenBlock(); myLastTokenBlock = myWrapper.getLastTokenBlock(); myCurrentBlock = myFirstTokenBlock; myTextRangeToWrapper = buildTextRangeToInfoMap(myFirstTokenBlock); myLastWhiteSpace = new WhiteSpace(getLastBlock().getEndOffset(), false); myLastWhiteSpace.append(myModel.getTextLength(), myModel, myDefaultIndentOption); } } private class AdjustWhiteSpacesState extends State { AdjustWhiteSpacesState() { super(FormattingStateId.PROCESSING_BLOCKS); } @Override protected void prepare() { } @Override protected void doIteration() { LeafBlockWrapper blockToProcess = myCurrentBlock; processToken(); if (blockToProcess != null) { myProgressCallback.afterProcessingBlock(blockToProcess); } if (myCurrentBlock != null) { return; } if (myAlignAgain.isEmpty()) { setDone(true); } else { myAlignAgain.clear(); myPreviousDependencies.clear(); myCurrentBlock = myFirstTokenBlock; } } } private class ApplyChangesState extends State { private final FormattingModel myModel; private List myBlocksToModify; private int myShift; private int myIndex; private boolean myResetBulkUpdateState; private ApplyChangesState(FormattingModel model) { super(FormattingStateId.APPLYING_CHANGES); myModel = model; } @Override protected void prepare() { myBlocksToModify = collectBlocksToModify(); // call doModifications static method to ensure no access to state // thus we may clear formatting state reset(); myInfos = null; myRootBlockWrapper = null; myTextRangeToWrapper = null; myPreviousDependencies = null; myLastWhiteSpace = null; myFirstTokenBlock = null; myLastTokenBlock = null; myDisposed = true; if (myBlocksToModify.isEmpty()) { setDone(true); return; } //for GeneralCodeFormatterTest if (myJavaIndentOptions == null) { myJavaIndentOptions = mySettings.getIndentOptions(StdFileTypes.JAVA); } myProgressCallback.beforeApplyingFormatChanges(myBlocksToModify); final int blocksToModifyCount = myBlocksToModify.size(); final boolean bulkReformat = blocksToModifyCount > 50; DocumentEx updatedDocument = bulkReformat ? getAffectedDocument(myModel) : null; if (updatedDocument != null) { updatedDocument.setInBulkUpdate(true); myResetBulkUpdateState = true; } if (blocksToModifyCount > BULK_REPLACE_OPTIMIZATION_CRITERIA && applyChangesAtRewriteMode(myBlocksToModify, myModel, myDefaultIndentOption)) { setDone(true); } } @Override protected void doIteration() { LeafBlockWrapper blockWrapper = myBlocksToModify.get(myIndex); myShift = replaceWhiteSpace( myModel, blockWrapper, myShift, blockWrapper.getWhiteSpace().generateWhiteSpace(getIndentOptionsToUse(blockWrapper, myDefaultIndentOption)), myDefaultIndentOption ); myProgressCallback.afterApplyingChange(blockWrapper); // block could be gc'd blockWrapper.getParent().dispose(); blockWrapper.dispose(); myBlocksToModify.set(myIndex, null); myIndex++; if (myIndex >= myBlocksToModify.size()) { setDone(true); } } @Override protected void setDone(boolean done) { super.setDone(done); if (myResetBulkUpdateState) { DocumentEx document = getAffectedDocument(myModel); if (document != null) { document.setInBulkUpdate(false); myResetBulkUpdateState = false; } } if (done) { myModel.commitChanges(); } } @Override public void stop() { if (myIndex > 0) { UIUtil.invokeAndWaitIfNeeded(new Runnable() { @Override public void run() { myModel.commitChanges(); } }); } } } }