diff --git a/platform/lang-impl/src/com/intellij/formatting/AbstractBlockWrapper.java b/platform/lang-impl/src/com/intellij/formatting/AbstractBlockWrapper.java index 8081b7f4a47e..0043dce2f854 100644 --- a/platform/lang-impl/src/com/intellij/formatting/AbstractBlockWrapper.java +++ b/platform/lang-impl/src/com/intellij/formatting/AbstractBlockWrapper.java @@ -337,7 +337,7 @@ public abstract class AbstractBlockWrapper { * * @return object that encapsulates information about number of symbols before the current block */ - protected abstract IndentData getNumberOfSymbolsBeforeBlock(); + public abstract IndentData getNumberOfSymbolsBeforeBlock(); /** * @return previous block for the current block if any; null otherwise diff --git a/platform/lang-impl/src/com/intellij/formatting/AlignmentImpl.java b/platform/lang-impl/src/com/intellij/formatting/AlignmentImpl.java index eebf46217aac..76ccf6819107 100644 --- a/platform/lang-impl/src/com/intellij/formatting/AlignmentImpl.java +++ b/platform/lang-impl/src/com/intellij/formatting/AlignmentImpl.java @@ -25,7 +25,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; -class AlignmentImpl extends Alignment { +public class AlignmentImpl extends Alignment { private static final List EMPTY = Collections.emptyList(); private final boolean myAllowBackwardShift; private final Anchor myAnchor; @@ -110,7 +110,7 @@ class AlignmentImpl extends Alignment { * {@link #setParent(Alignment) its parent} using the algorithm above if any; null otherwise */ @Nullable - LeafBlockWrapper getOffsetRespBlockBefore(@Nullable final AbstractBlockWrapper block) { + public LeafBlockWrapper getOffsetRespBlockBefore(@Nullable final AbstractBlockWrapper block) { if (!continueOffsetResponsibleBlockRetrieval(block)) { return null; } @@ -135,7 +135,7 @@ class AlignmentImpl extends Alignment { * * @param block wrapped block to register within the current alignment object */ - void setOffsetRespBlock(final LeafBlockWrapper block) { + public void setOffsetRespBlock(final LeafBlockWrapper block) { if (block == null) { return; } diff --git a/platform/lang-impl/src/com/intellij/formatting/CompositeBlockWrapper.java b/platform/lang-impl/src/com/intellij/formatting/CompositeBlockWrapper.java index 97508169a4de..fd5c95999d9f 100644 --- a/platform/lang-impl/src/com/intellij/formatting/CompositeBlockWrapper.java +++ b/platform/lang-impl/src/com/intellij/formatting/CompositeBlockWrapper.java @@ -75,7 +75,7 @@ public class CompositeBlockWrapper extends AbstractBlockWrapper{ } @Override - protected IndentData getNumberOfSymbolsBeforeBlock() { + public IndentData getNumberOfSymbolsBeforeBlock() { if (myChildren == null || myChildren.isEmpty()) { return new IndentData(0, 0); } diff --git a/platform/lang-impl/src/com/intellij/formatting/DependantSpacingImpl.java b/platform/lang-impl/src/com/intellij/formatting/DependantSpacingImpl.java index 4b9d8c1023b7..c56ce5f376f0 100644 --- a/platform/lang-impl/src/com/intellij/formatting/DependantSpacingImpl.java +++ b/platform/lang-impl/src/com/intellij/formatting/DependantSpacingImpl.java @@ -16,6 +16,7 @@ package com.intellij.formatting; +import com.intellij.formatting.engine.BlockRangesMap; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.containers.ContainerUtil; @@ -88,14 +89,14 @@ public class DependantSpacingImpl extends SpacingImpl { } @Override - public void refresh(FormatProcessor formatter) { + public void refresh(BlockRangesMap helper) { if (isDependentRegionLinefeedStatusChanged()) { return; } boolean atLeastOneDependencyRangeContainsLf = false; for (TextRange dependency : myDependentRegionRanges) { - atLeastOneDependencyRangeContainsLf |= formatter.containsLineFeeds(dependency); + atLeastOneDependencyRangeContainsLf |= helper.containsLineFeeds(dependency); } if (atLeastOneDependencyRangeContainsLf) myFlags |= DEPENDENCE_CONTAINS_LF_MASK; diff --git a/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java b/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java index ece6987525b1..caf00cbf450e 100644 --- a/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java +++ b/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java @@ -16,157 +16,30 @@ package com.intellij.formatting; -import com.intellij.diagnostic.LogMessageEx; -import com.intellij.lang.ASTNode; -import com.intellij.lang.Language; +import com.intellij.formatting.engine.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.EditorFactory; -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.Condition; -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.containers.ContainerUtil; -import com.intellij.util.containers.MultiMap; -import com.intellij.util.ui.UIUtil; -import gnu.trove.TIntObjectHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.List; -import static com.intellij.formatting.AbstractBlockAlignmentProcessor.Context; +import static com.intellij.formatting.InitialInfoBuilder.prepareToBuildBlocksSequentially; public 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 Set myAlignmentsInsideRangesToModify = null; + + private final WrapBlocksState myWrapState; private boolean myReformatContext; - - 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#setDependentRegionLinefeedStatusChanged() 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(); + private final Document myDocument; + @NotNull private final FormattingProgressCallback myProgressCallback; - private WhiteSpace myLastWhiteSpace; - private boolean myDisposed; - private CommonCodeStyleSettings.IndentOptions myJavaIndentOptions; - private final int myRightMargin; - @NotNull - private State myCurrentState; - private MultiMap myExpandableIndents; - private int myTotalBlocksWithAlignments; - private int myBlockRollbacks; - - private AlignmentCyclesDetector myCyclesDetector; + private StateProcessor myStateProcessor; public FormatProcessor(final FormattingDocumentModel docModel, Block rootBlock, @@ -178,91 +51,39 @@ public class FormatProcessor { this(docModel, rootBlock, new FormatOptions(settings, indentOptions, affectedRanges, false), progressCallback); } - public FormatProcessor(FormattingDocumentModel model, + public FormatProcessor(final FormattingDocumentModel model, Block block, FormatOptions options, @NotNull FormattingProgressCallback callback) { myProgressCallback = callback; - myDefaultIndentOption = options.myIndentOptions; - mySettings = options.mySettings; + + CommonCodeStyleSettings.IndentOptions defaultIndentOption = options.myIndentOptions; + CodeStyleSettings settings = options.mySettings; + BlockIndentOptions blockIndentOptions = new BlockIndentOptions(settings, defaultIndentOption, block); + myDocument = model.getDocument(); myReformatContext = options.myReformatContext; - myCurrentState = new WrapBlocksState(block, model, options.myAffectedRanges, options.myInterestingOffset); - myRightMargin = getRightMargin(block); + + final InitialInfoBuilder builder = prepareToBuildBlocksSequentially(block, model, options, settings, defaultIndentOption, myProgressCallback); + myWrapState = new WrapBlocksState(builder, blockIndentOptions); + + myStateProcessor = new StateProcessor(myWrapState); } - - 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 BlockRangesMap getBlockRangesMap() { + return myWrapState.getBlockRangesMap(); } 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(); - ExpandChildrenIndent expandChildrenIndent = new ExpandChildrenIndent(); - ApplyChangesState applyChangesState = new ApplyChangesState(model); - - expandChildrenIndent.setNext(applyChangesState); - adjustState.setNext(expandChildrenIndent); - myCurrentState.setNext(adjustState); + myStateProcessor.setNextState(new AdjustWhiteSpacesState(myWrapState, myProgressCallback, myReformatContext)); + myStateProcessor.setNextState(new ExpandChildrenIndentState(myDocument, myWrapState)); + myStateProcessor.setNextState(new ApplyChangesState(model, myWrapState, myProgressCallback)); } else { formatWithoutRealModifications(false); @@ -270,52 +91,32 @@ public class FormatProcessor { } } - /** - * 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()) { + if (myStateProcessor.isDone()) { return true; } - myCurrentState.iteration(); - return myCurrentState.isDone(); + myStateProcessor.iteration(); + return myStateProcessor.isDone(); } /** * Asks current processor to stop any active sequential processing if any. */ public void stopSequentialProcessing() { - myCurrentState.stop(); + myStateProcessor.stop(); } public void formatWithoutRealModifications() { formatWithoutRealModifications(false); } - @SuppressWarnings({"WhileLoopSpinsOnField"}) public void formatWithoutRealModifications(boolean sequentially) { - AdjustWhiteSpacesState adjustSpace = new AdjustWhiteSpacesState(); - adjustSpace.setNext(new ExpandChildrenIndent()); - myCurrentState.setNext(adjustSpace); - + myStateProcessor.setNextState(new AdjustWhiteSpacesState(myWrapState, myProgressCallback, myReformatContext)); + myStateProcessor.setNextState(new ExpandChildrenIndentState(myDocument, myWrapState)); if (sequentially) { return; } - - doIterationsSynchronously(FormattingStateId.PROCESSING_BLOCKS); - } - - private void reset() { - myBackwardShiftedAlignedBlocks.clear(); - myAlignmentMappings.clear(); - myPreviousDependencies.clear(); - myWrapCandidate = null; - if (myRootBlockWrapper != null) { - myRootBlockWrapper.reset(); - } + doIterationsSynchronously(); } public void performModifications(FormattingModel model) { @@ -323,710 +124,33 @@ public class FormatProcessor { } public void performModifications(FormattingModel model, boolean sequentially) { - assert !myDisposed; - myCurrentState.setNext(new ApplyChangesState(model)); + myStateProcessor.setNextState(new ApplyChangesState(model, myWrapState, myProgressCallback)); if (sequentially) { return; } - doIterationsSynchronously(FormattingStateId.APPLYING_CHANGES); + doIterationsSynchronously(); } - /** - * 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; - } - - /** - * Performs formatter changes in a series of blocks, for each block a new contents of document is calculated - * and whole document is replaced in one operation. - * - * @param blocksToModify changes introduced by formatter - * @param model current formatting model - * @param indentOption indent options to use - */ - @SuppressWarnings({"deprecation"}) - private void applyChangesAtRewriteMode(@NotNull final List blocksToModify, - @NotNull final FormattingModel model, - @NotNull CommonCodeStyleSettings.IndentOptions indentOption) - { - FormattingDocumentModel documentModel = model.getDocumentModel(); - Document document = documentModel.getDocument(); - CaretOffsetUpdater caretOffsetUpdater = new CaretOffsetUpdater(document); - - if (document instanceof DocumentEx) ((DocumentEx)document).setInBulkUpdate(true); - try { - 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) { - caretOffsetUpdater.update(changes); - 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); - } - caretOffsetUpdater.update(changes); - CharSequence mergeResult = BulkChangesMerger.INSTANCE.mergeToCharSequence(document.getChars(), document.getTextLength(), changes); - document.replaceString(0, document.getTextLength(), mergeResult); - } - finally { - if (document instanceof DocumentEx) ((DocumentEx)document).setInBulkUpdate(false); - } - - caretOffsetUpdater.restoreCaretLocations(); - cleanupBlocks(blocksToModify); - } - - 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 = textRange.shiftRight(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 = block.getTextRange().shiftRight(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(block.getNode(), 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 void processToken() { - final SpacingImpl spaceProperty = myCurrentBlock.getSpaceProperty(); - final WhiteSpace whiteSpace = myCurrentBlock.getWhiteSpace(); - - if (isReformatSelectedRangesContext()) { - if (isCurrentBlockAlignmentUsedInRangesToModify() && whiteSpace.isReadOnly() && spaceProperty != null && !spaceProperty.isReadOnly()) { - whiteSpace.setReadOnly(false); - whiteSpace.setLineFeedsAreReadOnly(true); - } - } - - 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(); - } - - - final List ranges = getDependentRegionRangesAfterCurrentWhiteSpace(spaceProperty, whiteSpace); - if (!ranges.isEmpty()) { - registerUnresolvedDependentSpacingRanges(spaceProperty, ranges); - } - - if (!whiteSpace.isIsReadOnly() && shouldReformatPreviouslyLocatedDependentSpacing(whiteSpace)) { - myAlignAgain.add(whiteSpace); - } - else if (!myAlignAgain.isEmpty()) { - myAlignAgain.remove(whiteSpace); - } - - myCurrentBlock = myCurrentBlock.getNextBlock(); - } - - private boolean isReformatSelectedRangesContext() { - return myReformatContext && !ContainerUtil.isEmpty(myAlignmentsInsideRangesToModify); - } - - private boolean isCurrentBlockAlignmentUsedInRangesToModify() { - AbstractBlockWrapper block = myCurrentBlock; - AlignmentImpl alignment = myCurrentBlock.getAlignment(); - - while (alignment == null) { - block = block.getParent(); - if (block == null || block.getStartOffset() != myCurrentBlock.getStartOffset()) { - return false; - } - alignment = block.getAlignment(); - } - - return myAlignmentsInsideRangesToModify.contains(alignment); - } - - private boolean shouldReformatPreviouslyLocatedDependentSpacing(WhiteSpace space) { - final TextRange changed = space.getTextRange(); - final SortedMap sortedHeadMap = myPreviousDependencies.tailMap(changed); - - for (final Map.Entry entry : sortedHeadMap.entrySet()) { - final TextRange textRange = entry.getKey(); - - if (textRange.contains(changed)) { - final DependantSpacingImpl spacing = entry.getValue(); - if (spacing.isDependentRegionLinefeedStatusChanged()) { - continue; - } - - final boolean containedLineFeeds = spacing.getMinLineFeeds() > 0; - final boolean containsLineFeeds = containsLineFeeds(textRange); - - if (containedLineFeeds != containsLineFeeds) { - spacing.setDependentRegionLinefeedStatusChanged(); - return true; - } - } - } - - return false; - } - - private void registerUnresolvedDependentSpacingRanges(final SpacingImpl spaceProperty, List unprocessedRanges) { - final DependantSpacingImpl dependantSpaceProperty = (DependantSpacingImpl)spaceProperty; - if (dependantSpaceProperty.isDependentRegionLinefeedStatusChanged()) return; - - for (TextRange range: unprocessedRanges) { - myPreviousDependencies.put(range, dependantSpaceProperty); - } - } - - private static List getDependentRegionRangesAfterCurrentWhiteSpace(final SpacingImpl spaceProperty, - final WhiteSpace whiteSpace) - { - if (!(spaceProperty instanceof DependantSpacingImpl)) return ContainerUtil.emptyList(); - - if (whiteSpace.isReadOnly() || whiteSpace.isLineFeedsAreReadOnly()) return ContainerUtil.emptyList(); - - DependantSpacingImpl spacing = (DependantSpacingImpl)spaceProperty; - return ContainerUtil.filter(spacing.getDependentRegionRanges(), new Condition() { - @Override - public boolean value(TextRange dependencyRange) { - return whiteSpace.getStartOffset() < dependencyRange.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)); - final LeafBlockWrapper offsetResponsibleBlock = alignment.getOffsetRespBlockBefore(myCurrentBlock); - myCyclesDetector.registerOffsetResponsibleBlock(offsetResponsibleBlock); - BlockAlignmentProcessor.Result result = alignmentProcessor.applyAlignment(context); - 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); - - if (myCyclesDetector.isCycleDetected()) { - reportAlignmentProcessingError(context); - return true; - } - else { - myCyclesDetector.registerBlockRollback(myCurrentBlock); - 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; - } - } - - private static void reportAlignmentProcessingError(Context context) { - ASTNode node = context.targetBlock.getNode(); - Language language = node != null ? node.getPsi().getLanguage() : null; - LogMessageEx.error(LOG, - (language != null ? language.getDisplayName() + ": " : "") + - "Can't align block " + context.targetBlock, context.document.getText()); - } - - /** - * 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 getBlockAtOrAfter(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; + private void doIterationsSynchronously() { + while (!myStateProcessor.isDone()) { + myStateProcessor.iteration(); } } public void setAllWhiteSpacesAreReadOnly() { - LeafBlockWrapper current = myFirstTokenBlock; + LeafBlockWrapper current = myWrapState.getFirstBlock(); while (current != null) { current.getWhiteSpace().setReadOnly(true); current = current.getNextBlock(); } } - static class ChildAttributesInfo { + public static class ChildAttributesInfo { public final AbstractBlockWrapper parent; - final ChildAttributes attributes; - final int index; + public final ChildAttributes attributes; + public final int index; public ChildAttributesInfo(final AbstractBlockWrapper parent, final ChildAttributes attributes, final int index) { this.parent = parent; @@ -1036,15 +160,15 @@ public class FormatProcessor { } public IndentInfo getIndentAt(final int offset) { - processBlocksBefore(offset); - AbstractBlockWrapper parent = getParentFor(offset, myCurrentBlock); + LeafBlockWrapper current = processBlocksBefore(offset); + AbstractBlockWrapper parent = getParentFor(offset, current); if (parent == null) { - final LeafBlockWrapper previousBlock = myCurrentBlock.getPreviousBlock(); + final LeafBlockWrapper previousBlock = current.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); + final Block block = myWrapState.getBlockToInfoMap().get(parent); if (block == null) { return new IndentInfo(0, 0, 0); @@ -1055,7 +179,8 @@ public class FormatProcessor { return new IndentInfo(0, 0, 0); } - return adjustLineIndent(info.parent, info.attributes, info.index); + IndentAdjuster adjuster = myWrapState.getIndentAdjuster(); + return adjuster.adjustLineIndent(current, info); } @Nullable @@ -1100,40 +225,7 @@ public class FormatProcessor { 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; @@ -1177,8 +269,9 @@ public class FormatProcessor { @Nullable private AbstractBlockWrapper getPreviousIncompleteBlock(final LeafBlockWrapper block, final int offset) { if (block == null) { - if (myLastTokenBlock.isIncomplete()) { - return myLastTokenBlock; + LeafBlockWrapper lastTokenBlock = myWrapState.getLastBlock(); + if (lastTokenBlock.isIncomplete()) { + return lastTokenBlock; } else { return null; @@ -1263,343 +356,36 @@ public class FormatProcessor { } 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(); + + private LeafBlockWrapper processBlocksBefore(final int offset) { + AdjustWhiteSpacesState state = new AdjustWhiteSpacesState(myWrapState, myProgressCallback, myReformatContext); + state.prepare(); + + LeafBlockWrapper last = null; + while (!state.isDone() && state.getCurrentBlock().getStartOffset() < offset) { + last = state.getCurrentBlock(); + state.doIteration(); } + + return state.getCurrentBlock() != null ? state.getCurrentBlock() : last; } public LeafBlockWrapper getFirstTokenBlock() { - return myFirstTokenBlock; + return myWrapState.getFirstBlock(); } public WhiteSpace getLastWhiteSpace() { - return myLastWhiteSpace; + return myWrapState.getLastWhiteSpace(); } - - /** - * 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; - return newIndent.getSpacesCount(options) - 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 - ); - myWrapper.setCollectAlignmentsInsideFormattingRange(myReformatContext); - - myExpandableIndents = myWrapper.getExpandableIndentsBlocks(); - } - - @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); - int lastBlockOffset = getLastBlock().getEndOffset(); - myLastWhiteSpace = new WhiteSpace(lastBlockOffset, false); - myLastWhiteSpace.append(Math.max(lastBlockOffset, myWrapper.getEndOffset()), myModel, myDefaultIndentOption); - myAlignmentsInsideRangesToModify = myWrapper.getAlignmentsInsideRangeToModify(); - myTotalBlocksWithAlignments = myWrapper.getBlocksToAlign().values().size(); - - myCyclesDetector = new AlignmentCyclesDetector(myTotalBlocksWithAlignments); - } - } - - 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(); - if (blocksToModifyCount > BULK_REPLACE_OPTIMIZATION_CRITERIA) { - applyChangesAtRewriteMode(myBlocksToModify, myModel, myDefaultIndentOption); - setDone(true); - } - else if (blocksToModifyCount > 50) { - DocumentEx updatedDocument = getAffectedDocument(myModel); - if (updatedDocument != null) { - updatedDocument.setInBulkUpdate(true); - myResetBulkUpdateState = 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(); - } - }); - } - } - } - - private static class CaretOffsetUpdater { - private final Map myCaretOffsets = new HashMap(); - - private CaretOffsetUpdater(@NotNull Document document) { - Editor[] editors = EditorFactory.getInstance().getEditors(document); - for (Editor editor : editors) { - myCaretOffsets.put(editor, editor.getCaretModel().getOffset()); - } - } - - private void update(@NotNull List changes) { - BulkChangesMerger merger = BulkChangesMerger.INSTANCE; - for (Map.Entry entry : myCaretOffsets.entrySet()) { - entry.setValue(merger.updateOffset(entry.getValue(), changes)); - } - } - - private void restoreCaretLocations() { - for (Map.Entry entry : myCaretOffsets.entrySet()) { - entry.getKey().getCaretModel().moveToOffset(entry.getValue()); - } - } - } - - + public static class FormatOptions { - private CodeStyleSettings mySettings; - private CommonCodeStyleSettings.IndentOptions myIndentOptions; + public CodeStyleSettings mySettings; + public CommonCodeStyleSettings.IndentOptions myIndentOptions; - private FormatTextRanges myAffectedRanges; - private boolean myReformatContext; + public FormatTextRanges myAffectedRanges; + public boolean myReformatContext; - private int myInterestingOffset; + public int myInterestingOffset; public FormatOptions(CodeStyleSettings settings, CommonCodeStyleSettings.IndentOptions options, @@ -1620,192 +406,4 @@ public class FormatProcessor { myInterestingOffset = interestingOffset; } } - - private class ExpandChildrenIndent extends State { - private Iterator myIterator; - private MultiMap myBlocksToRealign = new MultiMap(); - - public ExpandChildrenIndent() { - super(FormattingStateId.EXPANDING_CHILDREN_INDENTS); - } - - @Override - protected void doIteration() { - if (myIterator == null) { - myIterator = myExpandableIndents.keySet().iterator(); - } - if (!myIterator.hasNext()) { - setDone(true); - return; - } - - final ExpandableIndent indent = myIterator.next(); - Collection blocksToExpandIndent = myExpandableIndents.get(indent); - if (shouldExpand(blocksToExpandIndent)) { - for (AbstractBlockWrapper block : blocksToExpandIndent) { - indent.setEnforceIndent(true); - reindentNewLineChildren(block); - indent.setEnforceIndent(false); - } - } - - restoreAlignments(myBlocksToRealign); - myBlocksToRealign.clear(); - } - - private void restoreAlignments(MultiMap blocks) { - for (Alignment alignment : blocks.keySet()) { - AlignmentImpl alignmentImpl = (AlignmentImpl)alignment; - if (!alignmentImpl.isAllowBackwardShift()) continue; - - Set toRealign = alignmentImpl.getOffsetResponsibleBlocks(); - arrangeSpaces(toRealign); - - LeafBlockWrapper rightMostBlock = getRightMostBlock(toRealign); - int maxSpacesBeforeBlock = rightMostBlock.getNumberOfSymbolsBeforeBlock().getTotalSpaces(); - int rightMostBlockLine = myDocument.getLineNumber(rightMostBlock.getStartOffset()); - - for (LeafBlockWrapper block : toRealign) { - int currentBlockLine = myDocument.getLineNumber(block.getStartOffset()); - if (currentBlockLine == rightMostBlockLine) continue; - - int blockIndent = block.getNumberOfSymbolsBeforeBlock().getTotalSpaces(); - int delta = maxSpacesBeforeBlock - blockIndent; - if (delta > 0) { - int newSpaces = block.getWhiteSpace().getTotalSpaces() + delta; - adjustSpacingToKeepAligned(block, newSpaces); - } - } - } - } - - private void adjustSpacingToKeepAligned(LeafBlockWrapper block, int newSpaces) { - WhiteSpace space = block.getWhiteSpace(); - SpacingImpl property = block.getSpaceProperty(); - if (property == null) return; - space.arrangeSpaces(new SpacingImpl(newSpaces, newSpaces, - property.getMinLineFeeds(), - property.isReadOnly(), - property.isSafe(), - property.shouldKeepLineFeeds(), - property.getKeepBlankLines(), - property.shouldKeepFirstColumn(), - property.getPrefLineFeeds())); - } - - private LeafBlockWrapper getRightMostBlock(Collection toRealign) { - int maxSpacesBeforeBlock = -1; - LeafBlockWrapper rightMostBlock = null; - - for (LeafBlockWrapper block : toRealign) { - int spaces = block.getNumberOfSymbolsBeforeBlock().getTotalSpaces(); - if (spaces > maxSpacesBeforeBlock) { - maxSpacesBeforeBlock = spaces; - rightMostBlock = block; - } - } - - return rightMostBlock; - } - - private void arrangeSpaces(Collection toRealign) { - for (LeafBlockWrapper block : toRealign) { - WhiteSpace whiteSpace = block.getWhiteSpace(); - SpacingImpl spacing = block.getSpaceProperty(); - whiteSpace.arrangeSpaces(spacing); - } - } - - private boolean shouldExpand(Collection blocksToExpandIndent) { - AbstractBlockWrapper last = null; - for (AbstractBlockWrapper block : blocksToExpandIndent) { - if (block.getWhiteSpace().containsLineFeeds()) { - return true; - } - last = block; - } - - if (last != null) { - AbstractBlockWrapper next = getNextBlock(last); - if (next != null && next.getWhiteSpace().containsLineFeeds()) { - int nextNewLineBlockIndent = next.getNumberOfSymbolsBeforeBlock().getTotalSpaces(); - if (nextNewLineBlockIndent >= finMinNewLineIndent(blocksToExpandIndent)) { - return true; - } - } - } - - return false; - } - - private int finMinNewLineIndent(@NotNull Collection wrappers) { - int totalMinimum = Integer.MAX_VALUE; - for (AbstractBlockWrapper wrapper : wrappers) { - int minNewLineIndent = findMinNewLineIndent(wrapper); - if (minNewLineIndent < totalMinimum) { - totalMinimum = minNewLineIndent; - } - } - return totalMinimum; - } - - private int findMinNewLineIndent(@NotNull AbstractBlockWrapper block) { - if (block instanceof LeafBlockWrapper && block.getWhiteSpace().containsLineFeeds()) { - return block.getNumberOfSymbolsBeforeBlock().getTotalSpaces(); - } - else if (block instanceof CompositeBlockWrapper) { - List children = ((CompositeBlockWrapper)block).getChildren(); - int currentMin = Integer.MAX_VALUE; - for (AbstractBlockWrapper child : children) { - int childIndent = findMinNewLineIndent(child); - if (childIndent < currentMin) { - currentMin = childIndent; - } - } - return currentMin; - } - return Integer.MAX_VALUE; - } - - private AbstractBlockWrapper getNextBlock(AbstractBlockWrapper block) { - List children = block.getParent().getChildren(); - int nextBlockIndex = children.indexOf(block) + 1; - if (nextBlockIndex < children.size()) { - return children.get(nextBlockIndex); - } - return null; - } - - private void reindentNewLineChildren(final @NotNull AbstractBlockWrapper block) { - if (block instanceof LeafBlockWrapper) { - WhiteSpace space = block.getWhiteSpace(); - - if (space.containsLineFeeds()) { - myCurrentBlock = (LeafBlockWrapper)block; - adjustIndent(); - storeAlignmentsAfterCurrentBlock(); - } - } - else if (block instanceof CompositeBlockWrapper) { - List children = ((CompositeBlockWrapper)block).getChildren(); - for (AbstractBlockWrapper childBlock : children) { - reindentNewLineChildren(childBlock); - } - } - } - - private void storeAlignmentsAfterCurrentBlock() { - LeafBlockWrapper current = myCurrentBlock.getNextBlock(); - while (current != null && !current.getWhiteSpace().containsLineFeeds()) { - if (current.getAlignment() != null) { - myBlocksToRealign.putValue(current.getAlignment(), current); - } - current = current.getNextBlock(); - } - } - - @Override - protected void prepare() { - } - } } diff --git a/platform/lang-impl/src/com/intellij/formatting/FormatterImpl.java b/platform/lang-impl/src/com/intellij/formatting/FormatterImpl.java index f8dbc4ed53f9..2fd4d92f865c 100644 --- a/platform/lang-impl/src/com/intellij/formatting/FormatterImpl.java +++ b/platform/lang-impl/src/com/intellij/formatting/FormatterImpl.java @@ -16,6 +16,7 @@ package com.intellij.formatting; +import com.intellij.formatting.engine.ExpandableIndent; import com.intellij.lang.ASTNode; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; @@ -181,8 +182,6 @@ public class FormatterImpl extends FormatterEx FormatProcessor processor = new FormatProcessor( model.getDocumentModel(), model.getRootBlock(), settings, indentOptions, affectedRanges, FormattingProgressCallback.EMPTY ); - processor.setJavaIndentOptions(javaIndentOptions); - processor.format(model); return processor; } @@ -306,21 +305,6 @@ public class FormatterImpl extends FormatterEx execute(task); } - /** - * Execute given sequential formatting task. Two approaches are possible: - *

-   * 
    - *
  • - * synchronous - the task is completely executed during the current method processing; - *
  • - *
  • - * asynchronous - the task is executed at background thread under the progress dialog; - *
  • - *
- *
- * - * @param task task to execute - */ private void execute(@NotNull SequentialTask task) { disableFormatting(); Application application = ApplicationManager.getApplication(); @@ -363,7 +347,7 @@ public class FormatterImpl extends FormatterEx final FormatProcessor processor = buildProcessorAndWrapBlocks( model, block, settings, indentOptions, new FormatTextRanges(affectedRange, true) ); - final LeafBlockWrapper blockBefore = processor.getBlockAtOrAfter(affectedRange.getStartOffset()); + final LeafBlockWrapper blockBefore = processor.getBlockRangesMap().getBlockAtOrAfter(affectedRange.getStartOffset()); LOG.assertTrue(blockBefore != null); WhiteSpace whiteSpace = blockBefore.getWhiteSpace(); LOG.assertTrue(whiteSpace != null); @@ -473,7 +457,7 @@ public class FormatterImpl extends FormatterEx documentModel, block, settings, indentOptions, new FormatTextRanges(affectedRange, true), offset ); - final LeafBlockWrapper blockAfterOffset = processor.getBlockAtOrAfter(offset); + final LeafBlockWrapper blockAfterOffset = processor.getBlockRangesMap().getBlockAtOrAfter(offset); if (blockAfterOffset != null && blockAfterOffset.contains(offset)) { return offset; @@ -492,40 +476,14 @@ public class FormatterImpl extends FormatterEx return offset; } - /** - * Delegates to - * {@link #buildProcessorAndWrapBlocks(FormattingDocumentModel, Block, CodeStyleSettings, CommonCodeStyleSettings.IndentOptions, FormatTextRanges, int)} - * with '-1' as an interested offset. - * - * @param docModel - * @param rootBlock - * @param settings - * @param indentOptions - * @param affectedRanges - * @return - */ private static FormatProcessor buildProcessorAndWrapBlocks(final FormattingDocumentModel docModel, Block rootBlock, CodeStyleSettings settings, CommonCodeStyleSettings.IndentOptions indentOptions, - @Nullable FormatTextRanges affectedRanges) - { + @Nullable FormatTextRanges affectedRanges) { return buildProcessorAndWrapBlocks(docModel, rootBlock, settings, indentOptions, affectedRanges, -1); } - - /** - * Builds {@link FormatProcessor} instance and asks it to wrap all {@link Block code blocks} - * {@link FormattingModel#getRootBlock() derived from the given model}. - * - * @param docModel target model - * @param rootBlock root block to process - * @param settings code style settings to use - * @param indentOptions indent options to use - * @param affectedRanges ranges to reformat - * @param interestingOffset interesting offset; '-1' if no particular offset has a special interest - * @return format processor instance with wrapped {@link Block code blocks} - */ - @SuppressWarnings({"StatementWithEmptyBody"}) + private static FormatProcessor buildProcessorAndWrapBlocks(final FormattingDocumentModel docModel, Block rootBlock, CodeStyleSettings settings, @@ -599,7 +557,7 @@ public class FormatterImpl extends FormatterEx final FormatProcessor processor = buildProcessorAndWrapBlocks( documentModel, block, settings, indentOptions, new FormatTextRanges(affectedRange, true), offset ); - final LeafBlockWrapper blockAfterOffset = processor.getBlockAtOrAfter(offset); + final LeafBlockWrapper blockAfterOffset = processor.getBlockRangesMap().getBlockAtOrAfter(offset); if (blockAfterOffset != null && !blockAfterOffset.contains(offset)) { final WhiteSpace whiteSpace = blockAfterOffset.getWhiteSpace(); diff --git a/platform/lang-impl/src/com/intellij/formatting/IndentImpl.java b/platform/lang-impl/src/com/intellij/formatting/IndentImpl.java index 314531d5714b..cbed59277f78 100644 --- a/platform/lang-impl/src/com/intellij/formatting/IndentImpl.java +++ b/platform/lang-impl/src/com/intellij/formatting/IndentImpl.java @@ -18,7 +18,7 @@ package com.intellij.formatting; import org.jetbrains.annotations.NonNls; -class IndentImpl extends Indent { +public class IndentImpl extends Indent { private final boolean myIsAbsolute; private final boolean myRelativeToDirectParent; diff --git a/platform/lang-impl/src/com/intellij/formatting/IndentInside.java b/platform/lang-impl/src/com/intellij/formatting/IndentInside.java index 709a08d7d3ee..b6b126f0812f 100644 --- a/platform/lang-impl/src/com/intellij/formatting/IndentInside.java +++ b/platform/lang-impl/src/com/intellij/formatting/IndentInside.java @@ -21,7 +21,7 @@ import com.intellij.util.text.CharArrayUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -class IndentInside { +public class IndentInside { public int whiteSpaces = 0; public int tabs = 0; @@ -66,7 +66,7 @@ class IndentInside { } @NotNull - static IndentInside createIndentOn(@Nullable final CharSequence lastLine) { + public static IndentInside createIndentOn(@Nullable final CharSequence lastLine) { final IndentInside result = new IndentInside(); if (lastLine == null) { return result; @@ -79,7 +79,7 @@ class IndentInside { } @NotNull - static CharSequence getLastLine(@NotNull final CharSequence text) { + public static CharSequence getLastLine(@NotNull final CharSequence text) { int i = CharArrayUtil.shiftBackwardUntil(text, text.length() - 1, "\n"); if (i < 0) { return text; diff --git a/platform/lang-impl/src/com/intellij/formatting/InitialInfoBuilder.java b/platform/lang-impl/src/com/intellij/formatting/InitialInfoBuilder.java index b9db5823b859..db44a57a373f 100644 --- a/platform/lang-impl/src/com/intellij/formatting/InitialInfoBuilder.java +++ b/platform/lang-impl/src/com/intellij/formatting/InitialInfoBuilder.java @@ -17,6 +17,7 @@ package com.intellij.formatting; import com.intellij.diagnostic.LogMessageEx; +import com.intellij.formatting.engine.ExpandableIndent; import com.intellij.lang.LanguageFormatting; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; @@ -46,7 +47,7 @@ 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. */ -class InitialInfoBuilder { +public class InitialInfoBuilder { private static final Logger LOG = Logger.getInstance("#com.intellij.formatting.InitialInfoBuilder"); private final Map myResult = new THashMap(); @@ -98,15 +99,16 @@ class InitialInfoBuilder { myFormatterTagHandler = new FormatterTagHandler(settings); } - protected static InitialInfoBuilder prepareToBuildBlocksSequentially(Block root, - FormattingDocumentModel model, - @Nullable final FormatTextRanges affectedRanges, - @NotNull CodeStyleSettings settings, - final CommonCodeStyleSettings.IndentOptions options, - int interestingOffset, - @NotNull FormattingProgressCallback progressCallback) + 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, affectedRanges, settings, options, interestingOffset, 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, true); return builder; } @@ -120,8 +122,12 @@ class InitialInfoBuilder { } return minOffset; } + + public FormattingDocumentModel getFormattingDocumentModel() { + return myModel; + } - int getEndOffset() { + public int getEndOffset() { int maxDocOffset = myModel.getTextLength(); int maxOffset = myRootBlockWrapper != null ? myRootBlockWrapper.getEndOffset() : 0; if (myAffectedRanges != null) { diff --git a/platform/lang-impl/src/com/intellij/formatting/LeafBlockWrapper.java b/platform/lang-impl/src/com/intellij/formatting/LeafBlockWrapper.java index 8ba16b4ddddd..ab3ef322a786 100644 --- a/platform/lang-impl/src/com/intellij/formatting/LeafBlockWrapper.java +++ b/platform/lang-impl/src/com/intellij/formatting/LeafBlockWrapper.java @@ -21,7 +21,7 @@ import com.intellij.openapi.util.TextRange; import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import org.jetbrains.annotations.Nullable; -class LeafBlockWrapper extends AbstractBlockWrapper { +public class LeafBlockWrapper extends AbstractBlockWrapper { private static final int CONTAIN_LINE_FEEDS = 4; private static final int READ_ONLY = 8; private static final int LEAF = 16; @@ -126,7 +126,7 @@ class LeafBlockWrapper extends AbstractBlockWrapper { } @Override - protected IndentData getNumberOfSymbolsBeforeBlock() { + public IndentData getNumberOfSymbolsBeforeBlock() { int spaces = getWhiteSpace().getSpaces(); int indentSpaces = getWhiteSpace().getIndentSpaces(); diff --git a/platform/lang-impl/src/com/intellij/formatting/SpacingImpl.java b/platform/lang-impl/src/com/intellij/formatting/SpacingImpl.java index 06334cb58660..d0009f7aeb5e 100644 --- a/platform/lang-impl/src/com/intellij/formatting/SpacingImpl.java +++ b/platform/lang-impl/src/com/intellij/formatting/SpacingImpl.java @@ -16,13 +16,14 @@ package com.intellij.formatting; +import com.intellij.formatting.engine.BlockRangesMap; import org.jetbrains.annotations.NonNls; /** * Extends {@link Spacing} in order to keep number of additional settings like 'minSpaces', 'minLineFeeds', * 'prefLineFeeds' etc. */ -class SpacingImpl extends Spacing { +public class SpacingImpl extends Spacing { private int myMinSpaces; private int myKeepBlankLines; private int myMaxSpaces; @@ -76,7 +77,7 @@ class SpacingImpl extends Spacing { return myMinLineFeeds; } - final boolean isReadOnly(){ + public final boolean isReadOnly(){ return (myFlags & READ_ONLY_MASK) != 0; } @@ -90,10 +91,8 @@ class SpacingImpl extends Spacing { /** * Allows to ask to refresh current state using given formatter if necessary. - * - * @param formatter formatter to use during state refresh */ - public void refresh(FormatProcessor formatter) { + public void refresh(BlockRangesMap helper) { } public final boolean shouldKeepLineFeeds() { diff --git a/platform/lang-impl/src/com/intellij/formatting/WhiteSpace.java b/platform/lang-impl/src/com/intellij/formatting/WhiteSpace.java index 63692a899925..7bee50ca0ea3 100644 --- a/platform/lang-impl/src/com/intellij/formatting/WhiteSpace.java +++ b/platform/lang-impl/src/com/intellij/formatting/WhiteSpace.java @@ -16,6 +16,7 @@ package com.intellij.formatting; +import com.intellij.formatting.engine.BlockRangesMap; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; @@ -45,7 +46,7 @@ import java.util.ArrayList; *

* Not thread-safe. */ -class WhiteSpace { +public class WhiteSpace { private static final char LINE_FEED = '\n'; @@ -392,16 +393,13 @@ class WhiteSpace { /** * Tries to ensure that number of line feeds managed by the current {@link WhiteSpace} is consistent to the settings * defined at the given spacing property. - * - * @param spaceProperty space settings holder - * @param formatProcessor format processor to use for space settings state refreshing */ - public void arrangeLineFeeds(final SpacingImpl spaceProperty, final FormatProcessor formatProcessor) { + public void arrangeLineFeeds(final SpacingImpl spaceProperty, final BlockRangesMap helper) { performModification(new Runnable() { @Override public void run() { if (spaceProperty != null) { - spaceProperty.refresh(formatProcessor); + spaceProperty.refresh(helper); if (spaceProperty.getMinLineFeeds() >= 0 && getLineFeeds() < spaceProperty.getMinLineFeeds()) { setLineFeeds(spaceProperty.getMinLineFeeds()); @@ -541,11 +539,8 @@ class WhiteSpace { *

* This method may be considered a shortcut for calling {@link #arrangeLineFeeds(SpacingImpl, FormatProcessor)} and * {@link #arrangeSpaces(SpacingImpl)}. - * - * @param spacing spacing settings holder - * @param formatProcessor format processor to use to refresh state of the given 'spacing' object */ - public void removeLineFeeds(final SpacingImpl spacing, final FormatProcessor formatProcessor) { + public void removeLineFeeds(final SpacingImpl spacing, final BlockRangesMap helper) { performModification(new Runnable() { @Override public void run() { @@ -554,7 +549,7 @@ class WhiteSpace { myIndentSpaces = 0; } }); - arrangeLineFeeds(spacing, formatProcessor); + arrangeLineFeeds(spacing, helper); arrangeSpaces(spacing); } diff --git a/platform/lang-impl/src/com/intellij/formatting/WrapImpl.java b/platform/lang-impl/src/com/intellij/formatting/WrapImpl.java index 7be44a8e6816..1eb07bf66265 100644 --- a/platform/lang-impl/src/com/intellij/formatting/WrapImpl.java +++ b/platform/lang-impl/src/com/intellij/formatting/WrapImpl.java @@ -20,7 +20,7 @@ import org.jetbrains.annotations.Nullable; import java.util.*; -class WrapImpl extends Wrap { +public class WrapImpl extends Wrap { /** * The block where the wrap needs to happen if the CHOP wrap mode is used and the chain of blocks exceeds the right margin. */ @@ -124,11 +124,11 @@ class WrapImpl extends Wrap { myIgnoredWraps.get(wrap).add(currentBlock); } - enum Type{ + public enum Type{ DO_NOT_WRAP, WRAP_AS_NEEDED, CHOP_IF_NEEDED, WRAP_ALWAYS } - LeafBlockWrapper getChopStartBlock() { + public LeafBlockWrapper getChopStartBlock() { return myChopStartBlock; } @@ -139,7 +139,7 @@ class WrapImpl extends Wrap { *

  • '{@link #isActive() isActive}' property value is set (to true)
  • * */ - void setActive() { + public void setActive() { myChopStartBlock = null; myFlags |= ACTIVE_MASK; } @@ -150,7 +150,7 @@ class WrapImpl extends Wrap { * * @param startOffset new '{@link #getWrapOffset() firstPosition}' property value to use if current value is undefined (negative) */ - void setWrapOffset(final int startOffset) { + public void setWrapOffset(final int startOffset) { if (myWrapOffset < 0) { myWrapOffset = startOffset; } @@ -160,7 +160,7 @@ class WrapImpl extends Wrap { * @return '{@link #getWrapOffset() firstPosition}' property value defined previously via {@link #setWrapOffset(int)} if any; * '-1' otherwise */ - int getWrapOffset() { + public int getWrapOffset() { return myWrapOffset; } @@ -180,7 +180,7 @@ class WrapImpl extends Wrap { myFlags |= (wrapFirstElement ? WRAP_FIRST_ELEMENT_MASK:0) | (myType.ordinal() << TYPE_SHIFT) | (myId << ID_SHIFT); } - final Type getType() { + public final Type getType() { return myTypes[(myFlags & TYPE_MASK) >>> TYPE_SHIFT]; } @@ -190,17 +190,17 @@ class WrapImpl extends Wrap { * * @return 'wrapFirstElement' property value */ - final boolean isWrapFirstElement() { + public final boolean isWrapFirstElement() { return (myFlags & WRAP_FIRST_ELEMENT_MASK) != 0; } - void saveChopBlock(LeafBlockWrapper current) { + public void saveChopBlock(LeafBlockWrapper current) { if (myChopStartBlock == null) { myChopStartBlock = current; } } - final boolean isActive() { + public final boolean isActive() { return (myFlags & ACTIVE_MASK) != 0; } diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/AdjustWhiteSpacesState.java b/platform/lang-impl/src/com/intellij/formatting/engine/AdjustWhiteSpacesState.java new file mode 100644 index 000000000000..1088d66132f8 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/AdjustWhiteSpacesState.java @@ -0,0 +1,228 @@ +/* + * Copyright 2000-2016 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 com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.TextRange; +import com.intellij.util.containers.ContainerUtil; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class AdjustWhiteSpacesState extends State { + + private final FormattingProgressCallback myProgressCallback; + + private WrapBlocksState myWrapBlocksState; + + + private LeafBlockWrapper myCurrentBlock; + + private DependentSpacingEngine myDependentSpacingEngine; + private WrapProcessor myWrapProcessor; + private BlockRangesMap myBlockRangesMap; + private IndentAdjuster myIndentAdjuster; + + + private boolean myReformatContext; + private Set myAlignmentsInsideRangesToModify = null; + + private final HashSet myAlignAgain = new HashSet(); + private LeafBlockWrapper myFirstBlock; + + public AdjustWhiteSpacesState(WrapBlocksState state, + FormattingProgressCallback progressCallback, + boolean isReformatContext) { + myWrapBlocksState = state; + myProgressCallback = progressCallback; + myReformatContext = isReformatContext; + } + + @Override + public void prepare() { + if (myWrapBlocksState != null) { + myFirstBlock = myWrapBlocksState.getFirstBlock(); + myCurrentBlock = myFirstBlock; + myDependentSpacingEngine = myWrapBlocksState.getDependentSpacingEngine(); + myWrapProcessor = myWrapBlocksState.getWrapProcessor(); + myIndentAdjuster = myWrapBlocksState.getIndentAdjuster(); + myBlockRangesMap = myWrapBlocksState.getBlockRangesMap(); + myAlignmentsInsideRangesToModify = myWrapBlocksState.getAlignmentsInsideRangesToModify(); + } + } + + public AdjustWhiteSpacesState(LeafBlockWrapper firstBlock, + DependentSpacingEngine dependentSpacingEngine, + WrapProcessor wrapProcessor, + IndentAdjuster indentAdjuster, + BlockRangesMap blockRangesMap, + Set alignmentsInsideRangesToModify, + boolean isReformatContext, + FormattingProgressCallback progressCallback) + { + myWrapBlocksState = null; + myCurrentBlock = firstBlock; + myDependentSpacingEngine = dependentSpacingEngine; + myWrapProcessor = wrapProcessor; + myIndentAdjuster = indentAdjuster; + myBlockRangesMap = blockRangesMap; + myAlignmentsInsideRangesToModify = alignmentsInsideRangesToModify; + myReformatContext = isReformatContext; + myProgressCallback = progressCallback; + } + + public LeafBlockWrapper getCurrentBlock() { + return myCurrentBlock; + } + + @Override + public void doIteration() { + LeafBlockWrapper blockToProcess = myCurrentBlock; + processToken(); + if (blockToProcess != null) { + myProgressCallback.afterProcessingBlock(blockToProcess); + } + + if (myCurrentBlock != null) { + return; + } + + if (myAlignAgain.isEmpty()) { + setDone(true); + } + else { + myAlignAgain.clear(); + myDependentSpacingEngine.clear(); + myCurrentBlock = myFirstBlock; + } + } + + private boolean isReformatSelectedRangesContext() { + return myReformatContext && !ContainerUtil.isEmpty(myAlignmentsInsideRangesToModify); + } + + 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; + } + } + + private void onCurrentLineChanged() { + myWrapProcessor.onCurrentLineChanged(); + } + + private boolean isCurrentBlockAlignmentUsedInRangesToModify() { + AbstractBlockWrapper block = myCurrentBlock; + AlignmentImpl alignment = myCurrentBlock.getAlignment(); + + while (alignment == null) { + block = block.getParent(); + if (block == null || block.getStartOffset() != myCurrentBlock.getStartOffset()) { + return false; + } + alignment = block.getAlignment(); + } + + return myAlignmentsInsideRangesToModify.contains(alignment); + } + + private static List getDependentRegionRangesAfterCurrentWhiteSpace(final SpacingImpl spaceProperty, + final WhiteSpace whiteSpace) { + if (!(spaceProperty instanceof DependantSpacingImpl)) return ContainerUtil.emptyList(); + + if (whiteSpace.isReadOnly() || whiteSpace.isLineFeedsAreReadOnly()) return ContainerUtil.emptyList(); + + DependantSpacingImpl spacing = (DependantSpacingImpl)spaceProperty; + return ContainerUtil.filter(spacing.getDependentRegionRanges(), new Condition() { + @Override + public boolean value(TextRange dependencyRange) { + return whiteSpace.getStartOffset() < dependencyRange.getEndOffset(); + } + }); + } + + + private void processToken() { + final SpacingImpl spaceProperty = myCurrentBlock.getSpaceProperty(); + final WhiteSpace whiteSpace = myCurrentBlock.getWhiteSpace(); + + if (isReformatSelectedRangesContext()) { + if (isCurrentBlockAlignmentUsedInRangesToModify() && + whiteSpace.isReadOnly() && + spaceProperty != null && + !spaceProperty.isReadOnly()) { + whiteSpace.setReadOnly(false); + whiteSpace.setLineFeedsAreReadOnly(true); + } + } + + whiteSpace.arrangeLineFeeds(spaceProperty, myBlockRangesMap); + + if (!whiteSpace.containsLineFeeds()) { + whiteSpace.arrangeSpaces(spaceProperty); + } + + try { + LeafBlockWrapper newBlock = myWrapProcessor.processWrap(myCurrentBlock); + if (newBlock != null) { + myCurrentBlock = newBlock; + return; + } + } + finally { + if (whiteSpace.containsLineFeeds()) { + onCurrentLineChanged(); + } + } + + LeafBlockWrapper newCurrentBlock = myIndentAdjuster.adjustIndent(myCurrentBlock); + if (newCurrentBlock != null) { + myCurrentBlock = newCurrentBlock; + onCurrentLineChanged(); + return; + } + + defineAlignOffset(myCurrentBlock); + + if (myCurrentBlock.containsLineFeeds()) { + onCurrentLineChanged(); + } + + + final List ranges = getDependentRegionRangesAfterCurrentWhiteSpace(spaceProperty, whiteSpace); + if (!ranges.isEmpty()) { + myDependentSpacingEngine.registerUnresolvedDependentSpacingRanges(spaceProperty, ranges); + } + + if (!whiteSpace.isIsReadOnly() && myDependentSpacingEngine.shouldReformatPreviouslyLocatedDependentSpacing(whiteSpace)) { + myAlignAgain.add(whiteSpace); + } + else if (!myAlignAgain.isEmpty()) { + myAlignAgain.remove(whiteSpace); + } + + myCurrentBlock = myCurrentBlock.getNextBlock(); + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/AlignmentHelper.java b/platform/lang-impl/src/com/intellij/formatting/engine/AlignmentHelper.java new file mode 100644 index 000000000000..a5a48b202b15 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/AlignmentHelper.java @@ -0,0 +1,146 @@ +/* + * 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.diagnostic.LogMessageEx; +import 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.util.containers.ContainerUtil; +import com.intellij.util.containers.MultiMap; + +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class AlignmentHelper { + private static final Logger LOG = Logger.getInstance(AlignmentHelper.class); + + 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()); + } + + private final Set myAlignmentsToSkip = ContainerUtil.newHashSet(); + private final Document myDocument; + private final BlockIndentOptions myBlockIndentOptions; + + private final AlignmentCyclesDetector myCyclesDetector; + + private final Map> myBackwardShiftedAlignedBlocks = ContainerUtil.newHashMap(); + private final Map> myAlignmentMappings = ContainerUtil.newHashMap(); + + public AlignmentHelper(Document document, MultiMap blocksToAlign, BlockIndentOptions options) { + myDocument = document; + myBlockIndentOptions = options; + int totalBlocks = blocksToAlign.values().size(); + myCyclesDetector = new AlignmentCyclesDetector(totalBlocks); + } + + private static void reportAlignmentProcessingError(BlockAlignmentProcessor.Context context) { + ASTNode node = context.targetBlock.getNode(); + Language language = node != null ? node.getPsi().getLanguage() : null; + LogMessageEx.error(LOG, + (language != null ? language.getDisplayName() + ": " : "") + + "Can't align block " + context.targetBlock, context.document.getText()); + } + + public LeafBlockWrapper applyAlignment(final AlignmentImpl alignment, final LeafBlockWrapper currentBlock) { + 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 null; + } + + BlockAlignmentProcessor.Context context = new BlockAlignmentProcessor.Context( + myDocument, alignment, currentBlock, myAlignmentMappings, myBackwardShiftedAlignedBlocks, + myBlockIndentOptions.getIndentOptions(currentBlock)); + final LeafBlockWrapper offsetResponsibleBlock = alignment.getOffsetRespBlockBefore(currentBlock); + myCyclesDetector.registerOffsetResponsibleBlock(offsetResponsibleBlock); + BlockAlignmentProcessor.Result result = alignmentProcessor.applyAlignment(context); + switch (result) { + case TARGET_BLOCK_PROCESSED_NOT_ALIGNED: + return null; + case TARGET_BLOCK_ALIGNED: + storeAlignmentMapping(currentBlock); + return null; + case BACKWARD_BLOCK_ALIGNED: + if (offsetResponsibleBlock == null) { + return null; + } + Set blocksCausedRealignment = new HashSet(); + myBackwardShiftedAlignedBlocks.clear(); + myBackwardShiftedAlignedBlocks.put(offsetResponsibleBlock, blocksCausedRealignment); + blocksCausedRealignment.add(currentBlock); + storeAlignmentMapping(currentBlock, offsetResponsibleBlock); + if (myCyclesDetector.isCycleDetected()) { + reportAlignmentProcessingError(context); + return null; + } + myCyclesDetector.registerBlockRollback(currentBlock); + return offsetResponsibleBlock.getNextBlock(); + case RECURSION_DETECTED: + myAlignmentsToSkip.add(alignment); + return offsetResponsibleBlock; // Fall through to the 'register alignment to skip'. + case UNABLE_TO_ALIGN_BACKWARD_BLOCK: + myAlignmentsToSkip.add(alignment); + return null; + default: + return null; + } + } + + public boolean shouldSkip(AlignmentImpl alignment) { + return myAlignmentsToSkip.contains(alignment); + } + + 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); + } + + private void storeAlignmentMapping(LeafBlockWrapper currentBlock) { + AlignmentImpl alignment = null; + AbstractBlockWrapper block = currentBlock; + while (alignment == null && block != null) { + alignment = block.getAlignment(); + block = block.getParent(); + } + if (alignment != null) { + block = alignment.getOffsetRespBlockBefore(currentBlock); + if (block != null) { + storeAlignmentMapping(currentBlock, block); + } + } + } + + public void reset() { + myBackwardShiftedAlignedBlocks.clear(); + myAlignmentMappings.clear(); + } +} diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/ApplyChangesState.java b/platform/lang-impl/src/com/intellij/formatting/engine/ApplyChangesState.java new file mode 100644 index 000000000000..3f6eb0968ed6 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/ApplyChangesState.java @@ -0,0 +1,225 @@ +/* + * 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 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.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class ApplyChangesState extends State { + + /** + * 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 final FormattingModel myModel; + + private final FormattingProgressCallback myProgressCallback; + private final WrapBlocksState myWrapState; + + private List myBlocksToModify; + private int myShift; + private int myIndex; + private boolean myResetBulkUpdateState; + + private BlockIndentOptions myBlockIndentOptions; + + public ApplyChangesState(FormattingModel model, WrapBlocksState state, FormattingProgressCallback callback) { + myModel = model; + myWrapState = state; + myProgressCallback = callback; + myBlockIndentOptions = state.getBlockIndentOptions(); + } + + /** + * Performs formatter changes in a series of blocks, for each block a new contents of document is calculated + * and whole document is replaced in one operation. + * + * @param blocksToModify changes introduced by formatter + * @param model current formatting model + */ + @SuppressWarnings({"deprecation"}) + private void applyChangesAtRewriteMode(@NotNull final List blocksToModify, + @NotNull final FormattingModel model) { + FormattingDocumentModel documentModel = model.getDocumentModel(); + Document document = documentModel.getDocument(); + CaretOffsetUpdater caretOffsetUpdater = new CaretOffsetUpdater(document); + + if (document instanceof DocumentEx) ((DocumentEx)document).setInBulkUpdate(true); + try { + List changes = new ArrayList(); + int shift = 0; + int currentIterationShift = 0; + for (LeafBlockWrapper block : blocksToModify) { + WhiteSpace whiteSpace = block.getWhiteSpace(); + CharSequence newWs = documentModel.adjustWhiteSpaceIfNecessary( + whiteSpace.generateWhiteSpace(myBlockIndentOptions.getIndentOptions(block)), whiteSpace.getStartOffset(), + whiteSpace.getEndOffset(), block.getNode(), false + ); + if (changes.size() > 10000) { + caretOffsetUpdater.update(changes); + 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); + } + caretOffsetUpdater.update(changes); + CharSequence mergeResult = BulkChangesMerger.INSTANCE.mergeToCharSequence(document.getChars(), document.getTextLength(), changes); + document.replaceString(0, document.getTextLength(), mergeResult); + } + finally { + if (document instanceof DocumentEx) ((DocumentEx)document).setInBulkUpdate(false); + } + + caretOffsetUpdater.restoreCaretLocations(); + cleanupBlocks(blocksToModify); + } + + 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 List collectBlocksToModify() { + List blocksToModify = new ArrayList(); + LeafBlockWrapper firstBlock = myWrapState.getFirstBlock(); + for (LeafBlockWrapper block = firstBlock; block != null; block = block.getNextBlock()) { + final WhiteSpace whiteSpace = block.getWhiteSpace(); + if (!whiteSpace.isReadOnly()) { + final String newWhiteSpace = whiteSpace.generateWhiteSpace(myBlockIndentOptions.getIndentOptions(block)); + if (!whiteSpace.equalsToString(newWhiteSpace)) { + blocksToModify.add(block); + } + } + } + return blocksToModify; + } + + @Override + public void prepare() { + myBlocksToModify = collectBlocksToModify(); + // call doModifications static method to ensure no access to state + // thus we may clear formatting state + + //reset(); + //myDisposed = true; + + if (myBlocksToModify.isEmpty()) { + setDone(true); + return; + } + + myProgressCallback.beforeApplyingFormatChanges(myBlocksToModify); + + final int blocksToModifyCount = myBlocksToModify.size(); + if (blocksToModifyCount > BULK_REPLACE_OPTIMIZATION_CRITERIA) { + applyChangesAtRewriteMode(myBlocksToModify, myModel); + setDone(true); + } + else if (blocksToModifyCount > 50) { + DocumentEx updatedDocument = getAffectedDocument(myModel); + if (updatedDocument != null) { + updatedDocument.setInBulkUpdate(true); + myResetBulkUpdateState = true; + } + } + } + + @Override + protected void doIteration() { + LeafBlockWrapper blockWrapper = myBlocksToModify.get(myIndex); + myShift = FormatProcessorUtils.replaceWhiteSpace( + myModel, + blockWrapper, + myShift, + blockWrapper.getWhiteSpace().generateWhiteSpace(myBlockIndentOptions.getIndentOptions(blockWrapper)), + myBlockIndentOptions.getIndentOptions() + ); + 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(); + } + }); + } + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/BlockIndentOptions.java b/platform/lang-impl/src/com/intellij/formatting/engine/BlockIndentOptions.java new file mode 100644 index 000000000000..9cdb278fb594 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/BlockIndentOptions.java @@ -0,0 +1,78 @@ +/* + * 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.ASTBlock; +import com.intellij.formatting.AbstractBlockWrapper; +import com.intellij.formatting.Block; +import com.intellij.lang.ASTNode; +import com.intellij.lang.Language; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import org.jetbrains.annotations.NotNull; + +public class BlockIndentOptions { + private final CodeStyleSettings mySettings; + private final CommonCodeStyleSettings.IndentOptions myIndentOptions; + private final int myRightMargin; + + public BlockIndentOptions(@NotNull CodeStyleSettings settings, @NotNull CommonCodeStyleSettings.IndentOptions indentOptions, Block block) { + mySettings = settings; + myIndentOptions = indentOptions; + myRightMargin = calcRightMargin(block); + } + + public CommonCodeStyleSettings.IndentOptions getIndentOptions() { + return myIndentOptions; + } + + @NotNull + public CommonCodeStyleSettings.IndentOptions getIndentOptions(@NotNull AbstractBlockWrapper block) { + final Language language = block.getLanguage(); + if (language == null) { + return myIndentOptions; + } + final CommonCodeStyleSettings commonSettings = mySettings.getCommonSettings(language); + if (commonSettings == null) { + return myIndentOptions; + } + final CommonCodeStyleSettings.IndentOptions result = commonSettings.getIndentOptions(); + return result == null ? myIndentOptions : result; + } + + public int getRightMargin() { + return myRightMargin; + } + + private int calcRightMargin(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); + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/BlockRangesMap.java b/platform/lang-impl/src/com/intellij/formatting/engine/BlockRangesMap.java new file mode 100644 index 000000000000..422cf75a364f --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/BlockRangesMap.java @@ -0,0 +1,88 @@ +/* + * 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.LeafBlockWrapper; +import com.intellij.openapi.util.TextRange; +import gnu.trove.TIntObjectHashMap; +import org.jetbrains.annotations.Nullable; + +public class BlockRangesMap { + private final LeafBlockWrapper myLastBlock; + private TIntObjectHashMap myTextRangeToWrapper; + + public BlockRangesMap(LeafBlockWrapper first, LeafBlockWrapper last) { + myLastBlock = last; + myTextRangeToWrapper = buildTextRangeToInfoMap(first); + } + + 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 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 getBlockAtOrAfter(final int startOffset) { + int current = startOffset; + LeafBlockWrapper result = null; + while (current < myLastBlock.getEndOffset()) { + 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 myLastBlock; + } + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/CaretOffsetUpdater.java b/platform/lang-impl/src/com/intellij/formatting/engine/CaretOffsetUpdater.java new file mode 100644 index 000000000000..1423dfafb619 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/CaretOffsetUpdater.java @@ -0,0 +1,51 @@ +/* + * 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.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.TextChange; +import com.intellij.openapi.editor.impl.BulkChangesMerger; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CaretOffsetUpdater { + private final Map myCaretOffsets = new HashMap(); + + public CaretOffsetUpdater(@NotNull Document document) { + Editor[] editors = EditorFactory.getInstance().getEditors(document); + for (Editor editor : editors) { + myCaretOffsets.put(editor, editor.getCaretModel().getOffset()); + } + } + + public void update(@NotNull List changes) { + BulkChangesMerger merger = BulkChangesMerger.INSTANCE; + for (Map.Entry entry : myCaretOffsets.entrySet()) { + entry.setValue(merger.updateOffset(entry.getValue(), changes)); + } + } + + public void restoreCaretLocations() { + for (Map.Entry entry : myCaretOffsets.entrySet()) { + entry.getKey().getCaretModel().moveToOffset(entry.getValue()); + } + } + } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/DependentSpacingEngine.java b/platform/lang-impl/src/com/intellij/formatting/engine/DependentSpacingEngine.java new file mode 100644 index 000000000000..c3e1d1ac39cc --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/DependentSpacingEngine.java @@ -0,0 +1,109 @@ +/* + * 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.DependantSpacingImpl; +import com.intellij.formatting.SpacingImpl; +import com.intellij.formatting.WhiteSpace; +import com.intellij.openapi.util.TextRange; + +import java.util.*; + +/** + * 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#setDependentRegionLinefeedStatusChanged() mark} the dependent spacing as changed and schedule one more + * formatting iteration. + */ +public class DependentSpacingEngine { + private final BlockRangesMap myBlockRangesMap; + + 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; + } + }); + + public DependentSpacingEngine(BlockRangesMap helper) { + myBlockRangesMap = helper; + } + + public boolean shouldReformatPreviouslyLocatedDependentSpacing(WhiteSpace space) { + final TextRange changed = space.getTextRange(); + final SortedMap sortedHeadMap = myPreviousDependencies.tailMap(changed); + + for (final Map.Entry entry : sortedHeadMap.entrySet()) { + final TextRange textRange = entry.getKey(); + + if (textRange.contains(changed)) { + final DependantSpacingImpl spacing = entry.getValue(); + if (spacing.isDependentRegionLinefeedStatusChanged()) { + continue; + } + + final boolean containedLineFeeds = spacing.getMinLineFeeds() > 0; + final boolean containsLineFeeds = myBlockRangesMap.containsLineFeeds(textRange); + + if (containedLineFeeds != containsLineFeeds) { + spacing.setDependentRegionLinefeedStatusChanged(); + return true; + } + } + } + + return false; + } + + public void registerUnresolvedDependentSpacingRanges(final SpacingImpl spaceProperty, List unprocessedRanges) { + final DependantSpacingImpl dependantSpaceProperty = (DependantSpacingImpl)spaceProperty; + if (dependantSpaceProperty.isDependentRegionLinefeedStatusChanged()) return; + + for (TextRange range: unprocessedRanges) { + myPreviousDependencies.put(range, dependantSpaceProperty); + } + } + + public void clear() { + myPreviousDependencies.clear(); + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/ExpandChildrenIndentState.java b/platform/lang-impl/src/com/intellij/formatting/engine/ExpandChildrenIndentState.java new file mode 100644 index 000000000000..296a436760b3 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/ExpandChildrenIndentState.java @@ -0,0 +1,221 @@ +/* + * 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 com.intellij.openapi.editor.Document; +import com.intellij.util.containers.MultiMap; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +public class ExpandChildrenIndentState extends State { + private final Document myDocument; + private final WrapBlocksState myWrapState; + private IndentAdjuster myIndentAdjuster; + private MultiMap myExpandableIndents; + private LeafBlockWrapper myCurrentBlock; + + private Iterator myIterator; + private MultiMap myBlocksToRealign = new MultiMap(); + + public ExpandChildrenIndentState(Document document, WrapBlocksState state) { + myDocument = document; + myWrapState = state; + } + + @Override + public void prepare() { + myExpandableIndents = myWrapState.getExpandableIndent(); + myIndentAdjuster = myWrapState.getIndentAdjuster(); + myIterator = myExpandableIndents.keySet().iterator(); + } + + @Override + protected void doIteration() { + if (!myIterator.hasNext()) { + setDone(true); + return; + } + + final ExpandableIndent indent = myIterator.next(); + Collection blocksToExpandIndent = myExpandableIndents.get(indent); + if (shouldExpand(blocksToExpandIndent)) { + for (AbstractBlockWrapper block : blocksToExpandIndent) { + indent.setEnforceIndent(true); + reindentNewLineChildren(block); + indent.setEnforceIndent(false); + } + } + + restoreAlignments(myBlocksToRealign); + myBlocksToRealign.clear(); + } + + private void restoreAlignments(MultiMap blocks) { + for (Alignment alignment : blocks.keySet()) { + AlignmentImpl alignmentImpl = (AlignmentImpl)alignment; + if (!alignmentImpl.isAllowBackwardShift()) continue; + + Set toRealign = alignmentImpl.getOffsetResponsibleBlocks(); + arrangeSpaces(toRealign); + + LeafBlockWrapper rightMostBlock = getRightMostBlock(toRealign); + int maxSpacesBeforeBlock = rightMostBlock.getNumberOfSymbolsBeforeBlock().getTotalSpaces(); + int rightMostBlockLine = myDocument.getLineNumber(rightMostBlock.getStartOffset()); + + for (LeafBlockWrapper block : toRealign) { + int currentBlockLine = myDocument.getLineNumber(block.getStartOffset()); + if (currentBlockLine == rightMostBlockLine) continue; + + int blockIndent = block.getNumberOfSymbolsBeforeBlock().getTotalSpaces(); + int delta = maxSpacesBeforeBlock - blockIndent; + if (delta > 0) { + int newSpaces = block.getWhiteSpace().getTotalSpaces() + delta; + adjustSpacingToKeepAligned(block, newSpaces); + } + } + } + } + + private void adjustSpacingToKeepAligned(LeafBlockWrapper block, int newSpaces) { + WhiteSpace space = block.getWhiteSpace(); + SpacingImpl property = block.getSpaceProperty(); + if (property == null) return; + space.arrangeSpaces(new SpacingImpl(newSpaces, newSpaces, + property.getMinLineFeeds(), + property.isReadOnly(), + property.isSafe(), + property.shouldKeepLineFeeds(), + property.getKeepBlankLines(), + property.shouldKeepFirstColumn(), + property.getPrefLineFeeds())); + } + + private LeafBlockWrapper getRightMostBlock(Collection toRealign) { + int maxSpacesBeforeBlock = -1; + LeafBlockWrapper rightMostBlock = null; + + for (LeafBlockWrapper block : toRealign) { + int spaces = block.getNumberOfSymbolsBeforeBlock().getTotalSpaces(); + if (spaces > maxSpacesBeforeBlock) { + maxSpacesBeforeBlock = spaces; + rightMostBlock = block; + } + } + + return rightMostBlock; + } + + private void arrangeSpaces(Collection toRealign) { + for (LeafBlockWrapper block : toRealign) { + WhiteSpace whiteSpace = block.getWhiteSpace(); + SpacingImpl spacing = block.getSpaceProperty(); + whiteSpace.arrangeSpaces(spacing); + } + } + + private boolean shouldExpand(Collection blocksToExpandIndent) { + AbstractBlockWrapper last = null; + for (AbstractBlockWrapper block : blocksToExpandIndent) { + if (block.getWhiteSpace().containsLineFeeds()) { + return true; + } + last = block; + } + + if (last != null) { + AbstractBlockWrapper next = getNextBlock(last); + if (next != null && next.getWhiteSpace().containsLineFeeds()) { + int nextNewLineBlockIndent = next.getNumberOfSymbolsBeforeBlock().getTotalSpaces(); + if (nextNewLineBlockIndent >= finMinNewLineIndent(blocksToExpandIndent)) { + return true; + } + } + } + + return false; + } + + private int finMinNewLineIndent(@NotNull Collection wrappers) { + int totalMinimum = Integer.MAX_VALUE; + for (AbstractBlockWrapper wrapper : wrappers) { + int minNewLineIndent = findMinNewLineIndent(wrapper); + if (minNewLineIndent < totalMinimum) { + totalMinimum = minNewLineIndent; + } + } + return totalMinimum; + } + + private int findMinNewLineIndent(@NotNull AbstractBlockWrapper block) { + if (block instanceof LeafBlockWrapper && block.getWhiteSpace().containsLineFeeds()) { + return block.getNumberOfSymbolsBeforeBlock().getTotalSpaces(); + } + else if (block instanceof CompositeBlockWrapper) { + List children = ((CompositeBlockWrapper)block).getChildren(); + int currentMin = Integer.MAX_VALUE; + for (AbstractBlockWrapper child : children) { + int childIndent = findMinNewLineIndent(child); + if (childIndent < currentMin) { + currentMin = childIndent; + } + } + return currentMin; + } + return Integer.MAX_VALUE; + } + + private AbstractBlockWrapper getNextBlock(AbstractBlockWrapper block) { + List children = block.getParent().getChildren(); + int nextBlockIndex = children.indexOf(block) + 1; + if (nextBlockIndex < children.size()) { + return children.get(nextBlockIndex); + } + return null; + } + + private void reindentNewLineChildren(final @NotNull AbstractBlockWrapper block) { + if (block instanceof LeafBlockWrapper) { + WhiteSpace space = block.getWhiteSpace(); + + if (space.containsLineFeeds()) { + myCurrentBlock = (LeafBlockWrapper)block; + myIndentAdjuster.adjustIndent(myCurrentBlock); //since aligned block starts new line, it should not touch any other block + storeAlignmentsAfterCurrentBlock(); + } + } + else if (block instanceof CompositeBlockWrapper) { + List children = ((CompositeBlockWrapper)block).getChildren(); + for (AbstractBlockWrapper childBlock : children) { + reindentNewLineChildren(childBlock); + } + } + } + + private void storeAlignmentsAfterCurrentBlock() { + LeafBlockWrapper current = myCurrentBlock.getNextBlock(); + while (current != null && !current.getWhiteSpace().containsLineFeeds()) { + if (current.getAlignment() != null) { + myBlocksToRealign.putValue(current.getAlignment(), current); + } + current = current.getNextBlock(); + } + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/formatting/ExpandableIndent.java b/platform/lang-impl/src/com/intellij/formatting/engine/ExpandableIndent.java similarity index 92% rename from platform/lang-impl/src/com/intellij/formatting/ExpandableIndent.java rename to platform/lang-impl/src/com/intellij/formatting/engine/ExpandableIndent.java index 8b60c5fb6ba7..fd634ca383d8 100644 --- a/platform/lang-impl/src/com/intellij/formatting/ExpandableIndent.java +++ b/platform/lang-impl/src/com/intellij/formatting/engine/ExpandableIndent.java @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.intellij.formatting; +package com.intellij.formatting.engine; + +import com.intellij.formatting.IndentImpl; public class ExpandableIndent extends IndentImpl { private boolean myEnforceIndent; diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/FormatProcessorUtils.java b/platform/lang-impl/src/com/intellij/formatting/engine/FormatProcessorUtils.java new file mode 100644 index 000000000000..c6b8066edb26 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/FormatProcessorUtils.java @@ -0,0 +1,65 @@ +/* + * 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 com.intellij.openapi.util.TextRange; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import org.jetbrains.annotations.NotNull; + +public class FormatProcessorUtils { + + + private static int calcShift(@NotNull final IndentInside oldIndent, + @NotNull final IndentInside newIndent, + @NotNull final CommonCodeStyleSettings.IndentOptions options) + { + if (oldIndent.equals(newIndent)) return 0; + return newIndent.getSpacesCount(options) - oldIndent.getSpacesCount(options); + } + + public 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 = textRange.shiftRight(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 = block.getTextRange().shiftRight(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(block.getNode(), currentBlockRange, shiftInside); + shift += newBlockRange.getLength() - block.getLength(); + } + } + return shift; + } + +} diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/IndentAdjuster.java b/platform/lang-impl/src/com/intellij/formatting/engine/IndentAdjuster.java new file mode 100644 index 000000000000..8d4272916928 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/IndentAdjuster.java @@ -0,0 +1,143 @@ +/* + * 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 com.intellij.formatting.FormatProcessor; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import org.jetbrains.annotations.Nullable; + +public class IndentAdjuster { + private final AlignmentHelper myAlignmentHelper; + private final BlockIndentOptions myBlockIndentOptions; + + public IndentAdjuster(BlockIndentOptions blockIndentOptions, AlignmentHelper alignmentHelper) { + myAlignmentHelper = alignmentHelper; + myBlockIndentOptions = blockIndentOptions; + } + + /** + * Sometimes to align block we adjust whitespace of other block before. + * In such a case, we rollback to that block restarting formatting from there. + * + * @return new current block if we need to rollback, null otherwise + */ + public LeafBlockWrapper adjustIndent(LeafBlockWrapper block) { + AlignmentImpl alignment = CoreFormatterUtil.getAlignment(block); + WhiteSpace whiteSpace = block.getWhiteSpace(); + + if (alignment == null || myAlignmentHelper.shouldSkip(alignment)) { + if (whiteSpace.containsLineFeeds()) { + adjustSpacingByIndentOffset(block); + } + else { + whiteSpace.arrangeSpaces(block.getSpaceProperty()); + } + return null; + } + + return myAlignmentHelper.applyAlignment(alignment, block); + } + + private void adjustSpacingByIndentOffset(LeafBlockWrapper block) { + CommonCodeStyleSettings.IndentOptions options = myBlockIndentOptions.getIndentOptions(block); + IndentData offset = block.calculateOffset(options); + block.getWhiteSpace().setSpaces(offset.getSpaces(), offset.getIndentSpaces()); + } + + public void adjustLineIndent(LeafBlockWrapper myCurrentBlock) { + IndentData alignOffset = getAlignOffset(myCurrentBlock); + + if (alignOffset == null) { + adjustSpacingByIndentOffset(myCurrentBlock); + } + else { + myCurrentBlock.getWhiteSpace().setSpaces(alignOffset.getSpaces(), alignOffset.getIndentSpaces()); + } + } + + @Nullable + private static IndentData getAlignOffset(LeafBlockWrapper myCurrentBlock) { + 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 IndentInfo adjustLineIndent(LeafBlockWrapper currentBlock, FormatProcessor.ChildAttributesInfo info) { + AbstractBlockWrapper parent = info.parent; + ChildAttributes childAttributes = info.attributes; + int index = info.index; + + int alignOffset = getAlignOffsetBefore(childAttributes.getAlignment(), null); + if (alignOffset == -1) { + return parent.calculateChildOffset(myBlockIndentOptions.getIndentOptions(parent), childAttributes, index).createIndentInfo(); + } + else { + AbstractBlockWrapper indentedParentBlock = CoreFormatterUtil.getIndentedParentBlock(currentBlock); + 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; + } + } +} diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/State.java b/platform/lang-impl/src/com/intellij/formatting/engine/State.java new file mode 100644 index 000000000000..77f1760ae594 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/State.java @@ -0,0 +1,50 @@ +/* + * 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; + +public abstract class State { + + private boolean myDone; + private Runnable myOnDoneAction; + + public void iteration() { + if (!isDone()) { + doIteration(); + } + } + + public boolean isDone() { + return myDone; + } + + protected void setDone(boolean done) { + if (!myDone && done && myOnDoneAction != null) { + myOnDoneAction.run(); + } + myDone = done; + } + + public void stop() {} + + protected abstract void doIteration(); + + public void prepare() {} + + public void setOnDone(Runnable onDoneAction) { + myOnDoneAction = onDoneAction; + } + +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/StateProcessor.java b/platform/lang-impl/src/com/intellij/formatting/engine/StateProcessor.java new file mode 100644 index 000000000000..606d96e45826 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/StateProcessor.java @@ -0,0 +1,57 @@ +/* + * 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.util.containers.ContainerUtil; + +import java.util.List; + +public class StateProcessor { + + private final List myStates = ContainerUtil.newArrayList(); + private State myCurrentState; + + public StateProcessor(State initial) { + myCurrentState = initial; + } + + public void setNextState(State state) { + myStates.add(state); + } + + public boolean isDone() { + return myStates.isEmpty() && myCurrentState.isDone(); + } + + public void iteration() { + if (!myCurrentState.isDone()) { + myCurrentState.iteration(); + } + shiftStateIfNecessary(); + } + + private void shiftStateIfNecessary() { + if (myCurrentState.isDone() && !myStates.isEmpty()) { + myCurrentState = myStates.get(0); + myStates.remove(0); + myCurrentState.prepare(); + } + } + + public void stop() { + myCurrentState.stop(); + } +} diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/WrapBlocksState.java b/platform/lang-impl/src/com/intellij/formatting/engine/WrapBlocksState.java new file mode 100644 index 000000000000..aeb8097c1967 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/WrapBlocksState.java @@ -0,0 +1,136 @@ +/* + * 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 com.intellij.openapi.editor.Document; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import com.intellij.util.containers.MultiMap; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Set; + +public class WrapBlocksState extends State { + private final InitialInfoBuilder myWrapper; + private final BlockIndentOptions myBlockIndentOptions; + private WhiteSpace myLastWhiteSpace; + private BlockRangesMap myBlockRangesMap; + private DependentSpacingEngine myDependentSpacingEngine; + private AlignmentHelper myAlignmentHelper; + private IndentAdjuster myIndentAdjuster; + private WrapProcessor myWrapProcessor; + + public WrapBlocksState(@NotNull InitialInfoBuilder initialInfoBuilder, BlockIndentOptions blockIndentOptions) { + myWrapper = initialInfoBuilder; + myBlockIndentOptions = blockIndentOptions; + } + + @Override + public void doIteration() { + if (isDone()) { + return; + } + setDone(myWrapper.iteration()); + } + + public Map getBlockToInfoMap() { + return myWrapper.getBlockToInfoMap(); + } + + public LeafBlockWrapper getFirstBlock() { + assertDone(); + return myWrapper.getFirstTokenBlock(); + } + + public LeafBlockWrapper getLastBlock() { + assertDone(); + return myWrapper.getLastTokenBlock(); + } + + public WhiteSpace getLastWhiteSpace() { + assertDone(); + if (myLastWhiteSpace == null) { + int lastBlockOffset = getLastBlock().getEndOffset(); + myLastWhiteSpace = new WhiteSpace(lastBlockOffset, false); + FormattingDocumentModel model = myWrapper.getFormattingDocumentModel(); + CommonCodeStyleSettings.IndentOptions options = myBlockIndentOptions.getIndentOptions(); + myLastWhiteSpace.append(Math.max(lastBlockOffset, myWrapper.getEndOffset()), model, options); + } + return myLastWhiteSpace; + } + + public BlockRangesMap getBlockRangesMap() { + assertDone(); + if (myBlockRangesMap == null) { + myBlockRangesMap = new BlockRangesMap(getFirstBlock(), getLastBlock()); + } + return myBlockRangesMap; + } + + public DependentSpacingEngine getDependentSpacingEngine() { + assertDone(); + if (myDependentSpacingEngine == null) { + myDependentSpacingEngine = new DependentSpacingEngine(getBlockRangesMap()); + } + return myDependentSpacingEngine; + } + + public Set getAlignmentsInsideRangesToModify() { + assertDone(); + return myWrapper.getAlignmentsInsideRangeToModify(); + } + + public AlignmentHelper getAlignmentHelper() { + assertDone(); + if (myAlignmentHelper == null) { + Document document = myWrapper.getFormattingDocumentModel().getDocument(); + myAlignmentHelper = new AlignmentHelper(document, myWrapper.getBlocksToAlign(), myBlockIndentOptions); + } + return myAlignmentHelper; + } + + public IndentAdjuster getIndentAdjuster() { + assertDone(); + if (myIndentAdjuster == null) { + myIndentAdjuster = new IndentAdjuster(myBlockIndentOptions, getAlignmentHelper()); + } + return myIndentAdjuster; + } + + public MultiMap getExpandableIndent() { + assertDone(); + return myWrapper.getExpandableIndentsBlocks(); + } + + public WrapProcessor getWrapProcessor() { + assertDone(); + if (myWrapProcessor == null) { + int rightMargin = myBlockIndentOptions.getRightMargin(); + myWrapProcessor = new WrapProcessor(myBlockRangesMap, getIndentAdjuster(), rightMargin); + } + return myWrapProcessor; + } + + public BlockIndentOptions getBlockIndentOptions() { + return myBlockIndentOptions; + } + + private void assertDone() { + if (!isDone()) throw new IllegalStateException(); + } + +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/formatting/engine/WrapProcessor.java b/platform/lang-impl/src/com/intellij/formatting/engine/WrapProcessor.java new file mode 100644 index 000000000000..acce258962dd --- /dev/null +++ b/platform/lang-impl/src/com/intellij/formatting/engine/WrapProcessor.java @@ -0,0 +1,213 @@ +/* + * 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); + } + + /** + * @return true if {@link #myCurrentBlock currently processed wrapped block} doesn't contain line feeds and + * exceeds right margin; false otherwise + */ + private boolean lineOver(LeafBlockWrapper currentBlock) { + return !currentBlock.containsLineFeeds() && + CoreFormatterUtil.getStartColumn(currentBlock) + currentBlock.getLength() > myRightMargin; + } + + + /** + * 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(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 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 true if wrap of the {@link #myWrapCandidate} object (if any) may be replaced by the given wrap; + * 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 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; + } + +}