blob: 73358455bdcdf4e8e92bea69e15fac86752c12c0 [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.formatting;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.TextChange;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.impl.BulkChangesMerger;
import com.intellij.openapi.editor.impl.TextChangeImpl;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.util.ui.UIUtil;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
class FormatProcessor {
private static final Map<Alignment.Anchor, BlockAlignmentProcessor> ALIGNMENT_PROCESSORS =
new EnumMap<Alignment.Anchor, BlockAlignmentProcessor>(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.
* <p/>
* Current constant holds minimum number of changes that should trigger such <code>'replace whole text'</code> optimization.
*/
private static final int BULK_REPLACE_OPTIMIZATION_CRITERIA = 3000;
private static final Logger LOG = Logger.getInstance("#com.intellij.formatting.FormatProcessor");
private LeafBlockWrapper myCurrentBlock;
private Map<AbstractBlockWrapper, Block> myInfos;
private CompositeBlockWrapper myRootBlockWrapper;
private TIntObjectHashMap<LeafBlockWrapper> 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:
* <p/>
* <pre>
* int i1 = 1;
* int i2, i3 = 2;
* </pre>
* <p/>
* There is a possible case that <code>'i1'</code>, <code>'i2'</code> and <code>'i3'</code> blocks re-use
* the same alignment, hence, <code>'i1'</code> is shifted to right during <code>'i3'</code> processing but
* that causes <code>'i2'</code> to be shifted right as wll because it's aligned to <code>'i1'</code> that
* increases offset of <code>'i3'</code> that, in turn, causes backward shift of <code>'i1'</code> etc.
* <p/>
* This map remembers such backward shifts in order to be able to break such infinite cycles.
*/
private final Map<LeafBlockWrapper, Set<LeafBlockWrapper>> myBackwardShiftedAlignedBlocks
= new HashMap<LeafBlockWrapper, Set<LeafBlockWrapper>>();
private final Map<AbstractBlockWrapper, Set<AbstractBlockWrapper>> myAlignmentMappings
= new HashMap<AbstractBlockWrapper, Set<AbstractBlockWrapper>>();
/**
* 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.
* <p/>
* This container holds 'bad alignment' objects that should not be processed.
*/
private final Set<Alignment> myAlignmentsToSkip = new HashSet<Alignment>();
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.
* <p/>
* Example:
* <pre>
* int[] data = {1, 2, 3};
* </pre>
* We want to keep that in one line if possible but place curly braces on separate lines if the width is not enough:
* <pre>
* int[] data = { | &lt; right margin
* 1, 2, 3 |
* } |
* </pre>
* 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 - <code>'1'</code> block has dependent spacing that targets the whole
* <code>'{1, 2, 3}'</code> block. So, it's not possible to answer whether line feed should be used during processing block
* <code>'1'</code>.
* <p/>
* 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.
* <p/>
* Every time we detect that formatter changes 'has line feeds' status of such dependent region, we
* {@link DependantSpacingImpl#setDependentRegionChanged() mark} the dependent spacing as changed and schedule one more
* formatting iteration.
*/
private SortedMap<TextRange, DependantSpacingImpl> myPreviousDependencies =
new TreeMap<TextRange, DependantSpacingImpl>(new Comparator<TextRange>() {
@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<WhiteSpace> myAlignAgain = new HashSet<WhiteSpace>();
@NotNull
private final FormattingProgressCallback myProgressCallback;
private WhiteSpace myLastWhiteSpace;
private boolean myDisposed;
private CommonCodeStyleSettings.IndentOptions myJavaIndentOptions;
private final int myRightMargin;
@NotNull
private State myCurrentState;
public FormatProcessor(final FormattingDocumentModel docModel,
Block rootBlock,
CodeStyleSettings settings,
CommonCodeStyleSettings.IndentOptions indentOptions,
@Nullable FormatTextRanges affectedRanges,
@NotNull FormattingProgressCallback progressCallback)
{
this(docModel, rootBlock, settings, indentOptions, affectedRanges, -1, progressCallback);
}
public FormatProcessor(final FormattingDocumentModel docModel,
Block rootBlock,
CodeStyleSettings settings,
CommonCodeStyleSettings.IndentOptions indentOptions,
@Nullable FormatTextRanges affectedRanges,
int interestingOffset,
@NotNull FormattingProgressCallback progressCallback)
{
myProgressCallback = progressCallback;
myDefaultIndentOption = indentOptions;
mySettings = settings;
myDocument = docModel.getDocument();
myCurrentState = new WrapBlocksState(rootBlock, docModel, affectedRanges, interestingOffset);
myRightMargin = getRightMargin(rootBlock);
}
private int getRightMargin(Block rootBlock) {
Language language = null;
if (rootBlock instanceof ASTBlock) {
ASTNode node = ((ASTBlock)rootBlock).getNode();
if (node != null) {
PsiElement psiElement = node.getPsi();
if (psiElement.isValid()) {
PsiFile psiFile = psiElement.getContainingFile();
if (psiFile != null) {
language = psiFile.getViewProvider().getBaseLanguage();
}
}
}
}
return mySettings.getRightMargin(language);
}
private LeafBlockWrapper getLastBlock() {
LeafBlockWrapper result = myFirstTokenBlock;
while (result.getNextBlock() != null) {
result = result.getNextBlock();
}
return result;
}
private static TIntObjectHashMap<LeafBlockWrapper> buildTextRangeToInfoMap(final LeafBlockWrapper first) {
final TIntObjectHashMap<LeafBlockWrapper> result = new TIntObjectHashMap<LeafBlockWrapper>();
LeafBlockWrapper current = first;
while (current != null) {
result.put(current.getStartOffset(), current);
current = current.getNextBlock();
}
return result;
}
public void format(FormattingModel model) {
format(model, false);
}
/**
* Asks current processor to perform formatting.
* <p/>
* There are two processing approaches at the moment:
* <pre>
* <ul>
* <li>perform formatting during the current method call;</li>
* <li>
* split the whole formatting process to the set of fine-grained tasks and execute them sequentially during
* subsequent {@link #iteration()} calls;
* </li>
* </ul>
* </pre>
* <p/>
* Here is rationale for the second approach - formatting may introduce changes to the underlying document and IntelliJ IDEA
* is designed in a way that write access is allowed from EDT only. That means that every time we execute particular action
* from EDT we have no chance of performing any other actions from EDT simultaneously (e.g. we may want to show progress bar
* that reflects current formatting state but the progress bar can' bet updated if formatting is performed during a single long
* method call). So, we can interleave formatting iterations with GUI state updates.
*
* @param model target formatting model
* @param sequentially flag that indicates what kind of processing should be used
*/
public void format(FormattingModel model, boolean sequentially) {
if (sequentially) {
AdjustWhiteSpacesState adjustState = new AdjustWhiteSpacesState();
adjustState.setNext(new ApplyChangesState(model));
myCurrentState.setNext(adjustState);
}
else {
formatWithoutRealModifications(false);
performModifications(model, false);
}
}
/**
* Asks current processor to perform processing iteration
*
* @return <code>true</code> if the processing is finished; <code>false</code> otherwise
* @see #format(FormattingModel, boolean)
*/
public boolean iteration() {
if (myCurrentState.isDone()) {
return true;
}
myCurrentState.iteration();
return myCurrentState.isDone();
}
/**
* Asks current processor to stop any active sequential processing if any.
*/
public void stopSequentialProcessing() {
myCurrentState.stop();
}
public void formatWithoutRealModifications() {
formatWithoutRealModifications(false);
}
@SuppressWarnings({"WhileLoopSpinsOnField"})
public void formatWithoutRealModifications(boolean sequentially) {
myCurrentState.setNext(new AdjustWhiteSpacesState());
if (sequentially) {
return;
}
doIterationsSynchronously(FormattingStateId.PROCESSING_BLOCKS);
}
private void reset() {
myBackwardShiftedAlignedBlocks.clear();
myAlignmentMappings.clear();
myPreviousDependencies.clear();
myWrapCandidate = null;
if (myRootBlockWrapper != null) {
myRootBlockWrapper.reset();
}
}
public void performModifications(FormattingModel model) {
performModifications(model, false);
}
public void performModifications(FormattingModel model, boolean sequentially) {
assert !myDisposed;
myCurrentState.setNext(new ApplyChangesState(model));
if (sequentially) {
return;
}
doIterationsSynchronously(FormattingStateId.APPLYING_CHANGES);
}
/**
* Perform iterations against the {@link #myCurrentState current state} until it's {@link FormattingStateId type}
* is {@link FormattingStateId#getPreviousStates() less} or equal to the given state.
*
* @param state target state to process
*/
private void doIterationsSynchronously(@NotNull FormattingStateId state) {
while ((myCurrentState.getStateId() == state || state.getPreviousStates().contains(myCurrentState.getStateId()))
&& !myCurrentState.isDone())
{
myCurrentState.iteration();
}
}
public void setJavaIndentOptions(final CommonCodeStyleSettings.IndentOptions javaIndentOptions) {
myJavaIndentOptions = javaIndentOptions;
}
/**
* Decides whether applying formatter changes should be applied incrementally one-by-one or merge result should be
* constructed locally and the whole document text should be replaced. Performs such single bulk change if necessary.
*
* @param blocksToModify changes introduced by formatter
* @param model current formatting model
* @param indentOption indent options to use
* @return <code>true</code> if given changes are applied to the document (i.e. no further processing is required);
* <code>false</code> otherwise
*/
@SuppressWarnings({"deprecation"})
private boolean applyChangesAtRewriteMode(@NotNull final List<LeafBlockWrapper> blocksToModify,
@NotNull final FormattingModel model,
@NotNull CommonCodeStyleSettings.IndentOptions indentOption)
{
FormattingDocumentModel documentModel = model.getDocumentModel();
Document document = documentModel.getDocument();
if (document == null) {
return false;
}
List<TextChange> changes = new ArrayList<TextChange>();
int shift = 0;
int currentIterationShift = 0;
for (LeafBlockWrapper block : blocksToModify) {
WhiteSpace whiteSpace = block.getWhiteSpace();
CharSequence newWs = documentModel.adjustWhiteSpaceIfNecessary(
whiteSpace.generateWhiteSpace(getIndentOptionsToUse(block, indentOption)), whiteSpace.getStartOffset(),
whiteSpace.getEndOffset(), block.getNode(), false
);
if (changes.size() > 10000) {
CharSequence mergeResult = BulkChangesMerger.INSTANCE.mergeToCharSequence(document.getChars(), document.getTextLength(), changes);
document.replaceString(0, document.getTextLength(), mergeResult);
shift += currentIterationShift;
currentIterationShift = 0;
changes.clear();
}
TextChangeImpl change = new TextChangeImpl(newWs, whiteSpace.getStartOffset() + shift, whiteSpace.getEndOffset() + shift);
currentIterationShift += change.getDiff();
changes.add(change);
}
CharSequence mergeResult = BulkChangesMerger.INSTANCE.mergeToCharSequence(document.getChars(), document.getTextLength(), changes);
document.replaceString(0, document.getTextLength(), mergeResult);
cleanupBlocks(blocksToModify);
return true;
}
private static void cleanupBlocks(List<LeafBlockWrapper> blocks) {
for (LeafBlockWrapper block : blocks) {
block.getParent().dispose();
block.dispose();
}
blocks.clear();
}
@Nullable
private static DocumentEx getAffectedDocument(final FormattingModel model) {
final Document document = model.getDocumentModel().getDocument();
if (document instanceof DocumentEx) {
return (DocumentEx)document;
}
else {
return null;
}
}
private static int replaceWhiteSpace(final FormattingModel model,
@NotNull final LeafBlockWrapper block,
int shift,
final CharSequence _newWhiteSpace,
final CommonCodeStyleSettings.IndentOptions options
) {
final WhiteSpace whiteSpace = block.getWhiteSpace();
final TextRange textRange = whiteSpace.getTextRange();
final TextRange wsRange = shiftRange(textRange, shift);
final String newWhiteSpace = _newWhiteSpace.toString();
TextRange newWhiteSpaceRange = model instanceof FormattingModelEx
? ((FormattingModelEx) model).replaceWhiteSpace(wsRange, block.getNode(), newWhiteSpace)
: model.replaceWhiteSpace(wsRange, newWhiteSpace);
shift += newWhiteSpaceRange.getLength() - textRange.getLength();
if (block.isLeaf() && whiteSpace.containsLineFeeds() && block.containsLineFeeds()) {
final TextRange currentBlockRange = shiftRange(block.getTextRange(), shift);
IndentInside oldBlockIndent = whiteSpace.getInitialLastLineIndent();
IndentInside whiteSpaceIndent = IndentInside.createIndentOn(IndentInside.getLastLine(newWhiteSpace));
final int shiftInside = calcShift(oldBlockIndent, whiteSpaceIndent, options);
if (shiftInside != 0 || !oldBlockIndent.equals(whiteSpaceIndent)) {
final TextRange newBlockRange = model.shiftIndentInsideRange(currentBlockRange, shiftInside);
shift += newBlockRange.getLength() - block.getLength();
}
}
return shift;
}
@NotNull
private List<LeafBlockWrapper> collectBlocksToModify() {
List<LeafBlockWrapper> blocksToModify = new ArrayList<LeafBlockWrapper>();
for (LeafBlockWrapper block = myFirstTokenBlock; block != null; block = block.getNextBlock()) {
final WhiteSpace whiteSpace = block.getWhiteSpace();
if (!whiteSpace.isReadOnly()) {
final String newWhiteSpace = whiteSpace.generateWhiteSpace(getIndentOptionsToUse(block, myDefaultIndentOption));
if (!whiteSpace.equalsToString(newWhiteSpace)) {
blocksToModify.add(block);
}
}
}
return blocksToModify;
}
@NotNull
private CommonCodeStyleSettings.IndentOptions getIndentOptionsToUse(@NotNull AbstractBlockWrapper block,
@NotNull CommonCodeStyleSettings.IndentOptions fallbackIndentOptions)
{
final Language language = block.getLanguage();
if (language == null) {
return fallbackIndentOptions;
}
final CommonCodeStyleSettings commonSettings = mySettings.getCommonSettings(language);
if (commonSettings == null) {
return fallbackIndentOptions;
}
final CommonCodeStyleSettings.IndentOptions result = commonSettings.getIndentOptions();
return result == null ? fallbackIndentOptions : result;
}
private static TextRange shiftRange(final TextRange textRange, final int shift) {
return new TextRange(textRange.getStartOffset() + shift, textRange.getEndOffset() + shift);
}
private void processToken() {
final SpacingImpl spaceProperty = myCurrentBlock.getSpaceProperty();
final WhiteSpace whiteSpace = myCurrentBlock.getWhiteSpace();
whiteSpace.arrangeLineFeeds(spaceProperty, this);
if (!whiteSpace.containsLineFeeds()) {
whiteSpace.arrangeSpaces(spaceProperty);
}
try {
if (processWrap()) {
return;
}
}
finally {
if (whiteSpace.containsLineFeeds()) {
onCurrentLineChanged();
}
}
if (!adjustIndent()) {
return;
}
defineAlignOffset(myCurrentBlock);
if (myCurrentBlock.containsLineFeeds()) {
onCurrentLineChanged();
}
if (shouldSaveDependency(spaceProperty, whiteSpace)) {
saveDependency(spaceProperty);
}
if (!whiteSpace.isIsReadOnly() && shouldReformatBecauseOfBackwardDependency(whiteSpace.getTextRange())) {
myAlignAgain.add(whiteSpace);
}
else if (!myAlignAgain.isEmpty()) {
myAlignAgain.remove(whiteSpace);
}
myCurrentBlock = myCurrentBlock.getNextBlock();
}
private boolean shouldReformatBecauseOfBackwardDependency(TextRange changed) {
final SortedMap<TextRange, DependantSpacingImpl> sortedHeadMap = myPreviousDependencies.tailMap(changed);
boolean result = false;
for (final Map.Entry<TextRange, DependantSpacingImpl> entry : sortedHeadMap.entrySet()) {
final TextRange textRange = entry.getKey();
if (textRange.contains(changed)) {
final DependantSpacingImpl dependentSpacing = entry.getValue();
final boolean containedLineFeeds = dependentSpacing.getMinLineFeeds() > 0;
final boolean containsLineFeeds = containsLineFeeds(textRange);
if (containedLineFeeds != containsLineFeeds) {
dependentSpacing.setDependentRegionChanged();
result = true;
}
}
}
return result;
}
private void saveDependency(final SpacingImpl spaceProperty) {
final DependantSpacingImpl dependantSpaceProperty = (DependantSpacingImpl)spaceProperty;
final TextRange dependency = dependantSpaceProperty.getDependency();
if (dependantSpaceProperty.isDependentRegionChanged()) {
return;
}
myPreviousDependencies.put(dependency, dependantSpaceProperty);
}
private static boolean shouldSaveDependency(final SpacingImpl spaceProperty, WhiteSpace whiteSpace) {
if (!(spaceProperty instanceof DependantSpacingImpl)) return false;
if (whiteSpace.isReadOnly() || whiteSpace.isLineFeedsAreReadOnly()) return false;
final TextRange dependency = ((DependantSpacingImpl)spaceProperty).getDependency();
return whiteSpace.getStartOffset() < dependency.getEndOffset();
}
/**
* Processes the wrap of the current block.
*
* @return true if we have changed myCurrentBlock and need to restart its processing; false if myCurrentBlock is unchanged and we can
* continue processing
*/
private boolean processWrap() {
final SpacingImpl spacing = myCurrentBlock.getSpaceProperty();
final WhiteSpace whiteSpace = myCurrentBlock.getWhiteSpace();
final boolean wrapWasPresent = whiteSpace.containsLineFeeds();
if (wrapWasPresent) {
myFirstWrappedBlockOnLine = null;
if (!whiteSpace.containsLineFeedsInitially()) {
whiteSpace.removeLineFeeds(spacing, this);
}
}
final boolean wrapIsPresent = whiteSpace.containsLineFeeds();
final ArrayList<WrapImpl> 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 <code>true</code> if wrap of the {@link #myWrapCandidate} object (if any) may be replaced by the given wrap;
* <code>false</code> 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 <code>true</code> if current formatting iteration should be continued;
* <code>false</code> otherwise (e.g. if previously processed block is shifted inside this method for example
* because of specified alignment options)
*/
private boolean adjustIndent() {
AlignmentImpl alignment = CoreFormatterUtil.getAlignment(myCurrentBlock);
WhiteSpace whiteSpace = myCurrentBlock.getWhiteSpace();
if (alignment == null || myAlignmentsToSkip.contains(alignment)) {
if (whiteSpace.containsLineFeeds()) {
adjustSpacingByIndentOffset();
}
else {
whiteSpace.arrangeSpaces(myCurrentBlock.getSpaceProperty());
}
return true;
}
BlockAlignmentProcessor alignmentProcessor = ALIGNMENT_PROCESSORS.get(alignment.getAnchor());
if (alignmentProcessor == null) {
LOG.error(String.format("Can't find alignment processor for alignment anchor %s", alignment.getAnchor()));
return true;
}
BlockAlignmentProcessor.Context context = new BlockAlignmentProcessor.Context(
myDocument, alignment, myCurrentBlock, myAlignmentMappings, myBackwardShiftedAlignedBlocks,
getIndentOptionsToUse(myCurrentBlock, myDefaultIndentOption)
);
BlockAlignmentProcessor.Result result = alignmentProcessor.applyAlignment(context);
final LeafBlockWrapper offsetResponsibleBlock = alignment.getOffsetRespBlockBefore(myCurrentBlock);
switch (result) {
case TARGET_BLOCK_PROCESSED_NOT_ALIGNED: return true;
case TARGET_BLOCK_ALIGNED: storeAlignmentMapping(); return true;
case BACKWARD_BLOCK_ALIGNED:
if (offsetResponsibleBlock == null) {
return true;
}
Set<LeafBlockWrapper> blocksCausedRealignment = new HashSet<LeafBlockWrapper>();
myBackwardShiftedAlignedBlocks.clear();
myBackwardShiftedAlignedBlocks.put(offsetResponsibleBlock, blocksCausedRealignment);
blocksCausedRealignment.add(myCurrentBlock);
storeAlignmentMapping(myCurrentBlock, offsetResponsibleBlock);
myCurrentBlock = offsetResponsibleBlock.getNextBlock();
onCurrentLineChanged();
return false;
case RECURSION_DETECTED:
myCurrentBlock = offsetResponsibleBlock; // Fall through to the 'register alignment to skip'.
case UNABLE_TO_ALIGN_BACKWARD_BLOCK:
myAlignmentsToSkip.add(alignment);
return false;
default: return true;
}
}
/**
* We need to track blocks which white spaces are modified because of alignment rules.
* <p/>
* 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<AbstractBlockWrapper> wrappers = myAlignmentMappings.get(key);
if (wrappers == null) {
myAlignmentMappings.put(key, wrappers = new HashSet<AbstractBlockWrapper>());
}
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 <code>true</code> if it's ok to wrap at the currently processed block; <code>false</code> 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<WrapImpl> 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 <code>true</code> if {@link #myCurrentBlock currently processed wrapped block} doesn't contain line feeds and
* exceeds right margin; <code>false</code> 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; <code>null</code> otherwise
*/
@Nullable
private IndentData getAlignOffset() {
AbstractBlockWrapper current = myCurrentBlock;
while (true) {
final AlignmentImpl alignment = current.getAlignment();
LeafBlockWrapper offsetResponsibleBlock;
if (alignment != null && (offsetResponsibleBlock = alignment.getOffsetRespBlockBefore(myCurrentBlock)) != null) {
final WhiteSpace whiteSpace = offsetResponsibleBlock.getWhiteSpace();
if (whiteSpace.containsLineFeeds()) {
return new IndentData(whiteSpace.getIndentSpaces(), whiteSpace.getSpaces());
}
else {
final int offsetBeforeBlock = CoreFormatterUtil.getStartColumn(offsetResponsibleBlock);
final AbstractBlockWrapper indentedParentBlock = CoreFormatterUtil.getIndentedParentBlock(myCurrentBlock);
if (indentedParentBlock == null) {
return new IndentData(0, offsetBeforeBlock);
}
else {
final int parentIndent = indentedParentBlock.getWhiteSpace().getIndentOffset();
if (parentIndent > offsetBeforeBlock) {
return new IndentData(0, offsetBeforeBlock);
}
else {
return new IndentData(parentIndent, offsetBeforeBlock - parentIndent);
}
}
}
}
else {
current = current.getParent();
if (current == null || current.getStartOffset() != myCurrentBlock.getStartOffset()) return null;
}
}
}
public boolean containsLineFeeds(final TextRange dependency) {
LeafBlockWrapper child = myTextRangeToWrapper.get(dependency.getStartOffset());
if (child == null) return false;
if (child.containsLineFeeds()) return true;
final int endOffset = dependency.getEndOffset();
while (child.getEndOffset() < endOffset) {
child = child.getNextBlock();
if (child == null) return false;
if (child.getWhiteSpace().containsLineFeeds()) return true;
if (child.containsLineFeeds()) return true;
}
return false;
}
@Nullable
public LeafBlockWrapper getBlockAfter(final int startOffset) {
int current = startOffset;
LeafBlockWrapper result = null;
while (current < myLastWhiteSpace.getStartOffset()) {
final LeafBlockWrapper currentValue = myTextRangeToWrapper.get(current);
if (currentValue != null) {
result = currentValue;
break;
}
current++;
}
LeafBlockWrapper prevBlock = getPrevBlock(result);
if (prevBlock != null && prevBlock.contains(startOffset)) {
return prevBlock;
}
else {
return result;
}
}
@Nullable
private LeafBlockWrapper getPrevBlock(@Nullable final LeafBlockWrapper result) {
if (result != null) {
return result.getPreviousBlock();
}
else {
return myLastTokenBlock;
}
}
public void setAllWhiteSpacesAreReadOnly() {
LeafBlockWrapper current = myFirstTokenBlock;
while (current != null) {
current.getWhiteSpace().setReadOnly(true);
current = current.getNextBlock();
}
}
static class ChildAttributesInfo {
public final AbstractBlockWrapper parent;
final ChildAttributes attributes;
final int index;
public ChildAttributesInfo(final AbstractBlockWrapper parent, final ChildAttributes attributes, final int index) {
this.parent = parent;
this.attributes = attributes;
this.index = index;
}
}
public IndentInfo getIndentAt(final int offset) {
processBlocksBefore(offset);
AbstractBlockWrapper parent = getParentFor(offset, myCurrentBlock);
if (parent == null) {
final LeafBlockWrapper previousBlock = myCurrentBlock.getPreviousBlock();
if (previousBlock != null) parent = getParentFor(offset, previousBlock);
if (parent == null) return new IndentInfo(0, 0, 0);
}
int index = getNewChildPosition(parent, offset);
final Block block = myInfos.get(parent);
if (block == null) {
return new IndentInfo(0, 0, 0);
}
ChildAttributesInfo info = getChildAttributesInfo(block, index, parent);
if (info == null) {
return new IndentInfo(0, 0, 0);
}
return adjustLineIndent(info.parent, info.attributes, info.index);
}
@Nullable
private static ChildAttributesInfo getChildAttributesInfo(@NotNull final Block block,
final int index,
@Nullable AbstractBlockWrapper parent) {
if (parent == null) {
return null;
}
ChildAttributes childAttributes = block.getChildAttributes(index);
if (childAttributes == ChildAttributes.DELEGATE_TO_PREV_CHILD) {
final Block newBlock = block.getSubBlocks().get(index - 1);
AbstractBlockWrapper prevWrappedBlock;
if (parent instanceof CompositeBlockWrapper) {
prevWrappedBlock = ((CompositeBlockWrapper)parent).getChildren().get(index - 1);
}
else {
prevWrappedBlock = parent.getPreviousBlock();
}
return getChildAttributesInfo(newBlock, newBlock.getSubBlocks().size(), prevWrappedBlock);
}
else if (childAttributes == ChildAttributes.DELEGATE_TO_NEXT_CHILD) {
AbstractBlockWrapper nextWrappedBlock;
if (parent instanceof CompositeBlockWrapper) {
List<AbstractBlockWrapper> children = ((CompositeBlockWrapper)parent).getChildren();
if (children != null && index < children.size()) {
nextWrappedBlock = children.get(index);
}
else {
return null;
}
}
else {
nextWrappedBlock = ((LeafBlockWrapper)parent).getNextBlock();
}
return getChildAttributesInfo(block.getSubBlocks().get(index), 0, nextWrappedBlock);
}
else {
return new ChildAttributesInfo(parent, childAttributes, index);
}
}
private IndentInfo adjustLineIndent(final AbstractBlockWrapper parent, final ChildAttributes childAttributes, final int index) {
int alignOffset = getAlignOffsetBefore(childAttributes.getAlignment(), null);
if (alignOffset == -1) {
return parent.calculateChildOffset(getIndentOptionsToUse(parent, myDefaultIndentOption), childAttributes, index).createIndentInfo();
}
else {
AbstractBlockWrapper indentedParentBlock = CoreFormatterUtil.getIndentedParentBlock(myCurrentBlock);
if (indentedParentBlock == null) {
return new IndentInfo(0, 0, alignOffset);
}
else {
int indentOffset = indentedParentBlock.getWhiteSpace().getIndentOffset();
if (indentOffset > alignOffset) {
return new IndentInfo(0, 0, alignOffset);
}
else {
return new IndentInfo(0, indentOffset, alignOffset - indentOffset);
}
}
}
}
private static int getAlignOffsetBefore(@Nullable final Alignment alignment, @Nullable final LeafBlockWrapper blockAfter) {
if (alignment == null) return -1;
final LeafBlockWrapper alignRespBlock = ((AlignmentImpl)alignment).getOffsetRespBlockBefore(blockAfter);
if (alignRespBlock != null) {
return CoreFormatterUtil.getStartColumn(alignRespBlock);
}
else {
return -1;
}
}
private static int getNewChildPosition(final AbstractBlockWrapper parent, final int offset) {
AbstractBlockWrapper parentBlockToUse = getLastNestedCompositeBlockForSameRange(parent);
if (!(parentBlockToUse instanceof CompositeBlockWrapper)) return 0;
final List<AbstractBlockWrapper> subBlocks = ((CompositeBlockWrapper)parentBlockToUse).getChildren();
//noinspection ConstantConditions
if (subBlocks != null) {
for (int i = 0; i < subBlocks.size(); i++) {
AbstractBlockWrapper block = subBlocks.get(i);
if (block.getStartOffset() >= offset) return i;
}
return subBlocks.size();
}
else {
return 0;
}
}
@Nullable
private static AbstractBlockWrapper getParentFor(final int offset, AbstractBlockWrapper block) {
AbstractBlockWrapper current = block;
while (current != null) {
if (current.getStartOffset() < offset && current.getEndOffset() >= offset) {
return current;
}
current = current.getParent();
}
return null;
}
@Nullable
private AbstractBlockWrapper getParentFor(final int offset, LeafBlockWrapper block) {
AbstractBlockWrapper previous = getPreviousIncompleteBlock(block, offset);
if (previous != null) {
return getLastNestedCompositeBlockForSameRange(previous);
}
else {
return getParentFor(offset, (AbstractBlockWrapper)block);
}
}
@Nullable
private AbstractBlockWrapper getPreviousIncompleteBlock(final LeafBlockWrapper block, final int offset) {
if (block == null) {
if (myLastTokenBlock.isIncomplete()) {
return myLastTokenBlock;
}
else {
return null;
}
}
AbstractBlockWrapper current = block;
while (current.getParent() != null && current.getParent().getStartOffset() > offset) {
current = current.getParent();
}
if (current.getParent() == null) return null;
if (current.getEndOffset() <= offset) {
while (!current.isIncomplete() &&
current.getParent() != null &&
current.getParent().getEndOffset() <= offset) {
current = current.getParent();
}
if (current.isIncomplete()) return current;
}
if (current.getParent() == null) return null;
final List<AbstractBlockWrapper> subBlocks = current.getParent().getChildren();
final int index = subBlocks.indexOf(current);
if (index < 0) {
LOG.assertTrue(false);
}
if (index == 0) return null;
AbstractBlockWrapper currentResult = subBlocks.get(index - 1);
if (!currentResult.isIncomplete()) return null;
AbstractBlockWrapper lastChild = getLastChildOf(currentResult);
while (lastChild != null && lastChild.isIncomplete()) {
currentResult = lastChild;
lastChild = getLastChildOf(currentResult);
}
return currentResult;
}
@Nullable
private static AbstractBlockWrapper getLastChildOf(final AbstractBlockWrapper currentResult) {
AbstractBlockWrapper parentBlockToUse = getLastNestedCompositeBlockForSameRange(currentResult);
if (!(parentBlockToUse instanceof CompositeBlockWrapper)) return null;
final List<AbstractBlockWrapper> subBlocks = ((CompositeBlockWrapper)parentBlockToUse).getChildren();
if (subBlocks.isEmpty()) return null;
return subBlocks.get(subBlocks.size() - 1);
}
/**
* There is a possible case that particular block is a composite block that contains number of nested composite blocks
* that all target the same text range. This method allows to derive the most nested block that shares the same range (if any).
*
* @param block block to check
* @return the most nested block of the given one that shares the same text range if any; given block otherwise
*/
@NotNull
private static AbstractBlockWrapper getLastNestedCompositeBlockForSameRange(@NotNull final AbstractBlockWrapper block) {
if (!(block instanceof CompositeBlockWrapper)) {
return block;
}
AbstractBlockWrapper result = block;
AbstractBlockWrapper candidate = block;
while (true) {
List<AbstractBlockWrapper> subBlocks = ((CompositeBlockWrapper)candidate).getChildren();
if (subBlocks == null || subBlocks.size() != 1) {
break;
}
candidate = subBlocks.get(0);
if (candidate.getStartOffset() == block.getStartOffset() && candidate.getEndOffset() == block.getEndOffset()
&& candidate instanceof CompositeBlockWrapper)
{
result = candidate;
}
else {
break;
}
}
return result;
}
private void processBlocksBefore(final int offset) {
while (true) {
myAlignAgain.clear();
myCurrentBlock = myFirstTokenBlock;
while (myCurrentBlock != null && myCurrentBlock.getStartOffset() < offset) {
processToken();
if (myCurrentBlock == null) {
myCurrentBlock = myLastTokenBlock;
if (myCurrentBlock != null) {
myProgressCallback.afterProcessingBlock(myCurrentBlock);
}
break;
}
}
if (myAlignAgain.isEmpty()) return;
reset();
}
}
public LeafBlockWrapper getFirstTokenBlock() {
return myFirstTokenBlock;
}
public WhiteSpace getLastWhiteSpace() {
return myLastWhiteSpace;
}
/**
* Calculates difference in visual columns between the given indents.
*
* @param oldIndent old indent
* @param newIndent new indent
* @param options indent options to use
* @return difference in visual columns between the given indents
*/
private static int calcShift(@NotNull final IndentInside oldIndent,
@NotNull final IndentInside newIndent,
@NotNull final CommonCodeStyleSettings.IndentOptions options)
{
if (oldIndent.equals(newIndent)) return 0;
if (options.USE_TAB_CHARACTER) {
return (newIndent.tabs - oldIndent.getTabsCount(options)) * options.TAB_SIZE;
}
else {
return newIndent.whiteSpaces - oldIndent.getSpacesCount(options);
}
}
/**
* Utility method to use during debugging formatter processing.
*
* @return text that contains intermediate formatter-introduced changes (even not committed yet)
*/
@SuppressWarnings("UnusedDeclaration")
@NotNull
private String getCurrentText() {
StringBuilder result = new StringBuilder();
for (LeafBlockWrapper block = myFirstTokenBlock; block != null; block = block.getNextBlock()) {
result.append(block.getWhiteSpace().generateWhiteSpace(getIndentOptionsToUse(block, myDefaultIndentOption)));
result.append(myDocument.getCharsSequence().subSequence(block.getStartOffset(), block.getEndOffset()));
}
return result.toString();
}
private abstract class State {
private final FormattingStateId myStateId;
private State myNextState;
private boolean myDone;
protected State(FormattingStateId stateId) {
myStateId = stateId;
}
public void iteration() {
if (!isDone()) {
doIteration();
}
shiftStateIfNecessary();
}
public boolean isDone() {
return myDone;
}
protected void setDone(boolean done) {
myDone = done;
}
public void setNext(@NotNull State state) {
if (getStateId() == state.getStateId() || (myNextState != null && myNextState.getStateId() == state.getStateId())) {
return;
}
myNextState = state;
shiftStateIfNecessary();
}
public FormattingStateId getStateId() {
return myStateId;
}
public void stop() {
}
protected abstract void doIteration();
protected abstract void prepare();
private void shiftStateIfNecessary() {
if (isDone() && myNextState != null) {
myCurrentState = myNextState;
myNextState = null;
myCurrentState.prepare();
}
}
}
private class WrapBlocksState extends State {
private final InitialInfoBuilder myWrapper;
private final FormattingDocumentModel myModel;
WrapBlocksState(@NotNull Block root,
@NotNull FormattingDocumentModel model,
@Nullable final FormatTextRanges affectedRanges,
int interestingOffset)
{
super(FormattingStateId.WRAPPING_BLOCKS);
myModel = model;
myWrapper = InitialInfoBuilder.prepareToBuildBlocksSequentially(
root, model, affectedRanges, mySettings, myDefaultIndentOption, interestingOffset, myProgressCallback
);
}
@Override
protected void prepare() {
}
@Override
public void doIteration() {
if (isDone()) {
return;
}
setDone(myWrapper.iteration());
if (!isDone()) {
return;
}
myInfos = myWrapper.getBlockToInfoMap();
myRootBlockWrapper = myWrapper.getRootBlockWrapper();
myFirstTokenBlock = myWrapper.getFirstTokenBlock();
myLastTokenBlock = myWrapper.getLastTokenBlock();
myCurrentBlock = myFirstTokenBlock;
myTextRangeToWrapper = buildTextRangeToInfoMap(myFirstTokenBlock);
myLastWhiteSpace = new WhiteSpace(getLastBlock().getEndOffset(), false);
myLastWhiteSpace.append(myModel.getTextLength(), myModel, myDefaultIndentOption);
}
}
private class AdjustWhiteSpacesState extends State {
AdjustWhiteSpacesState() {
super(FormattingStateId.PROCESSING_BLOCKS);
}
@Override
protected void prepare() {
}
@Override
protected void doIteration() {
LeafBlockWrapper blockToProcess = myCurrentBlock;
processToken();
if (blockToProcess != null) {
myProgressCallback.afterProcessingBlock(blockToProcess);
}
if (myCurrentBlock != null) {
return;
}
if (myAlignAgain.isEmpty()) {
setDone(true);
}
else {
myAlignAgain.clear();
myPreviousDependencies.clear();
myCurrentBlock = myFirstTokenBlock;
}
}
}
private class ApplyChangesState extends State {
private final FormattingModel myModel;
private List<LeafBlockWrapper> myBlocksToModify;
private int myShift;
private int myIndex;
private boolean myResetBulkUpdateState;
private ApplyChangesState(FormattingModel model) {
super(FormattingStateId.APPLYING_CHANGES);
myModel = model;
}
@Override
protected void prepare() {
myBlocksToModify = collectBlocksToModify();
// call doModifications static method to ensure no access to state
// thus we may clear formatting state
reset();
myInfos = null;
myRootBlockWrapper = null;
myTextRangeToWrapper = null;
myPreviousDependencies = null;
myLastWhiteSpace = null;
myFirstTokenBlock = null;
myLastTokenBlock = null;
myDisposed = true;
if (myBlocksToModify.isEmpty()) {
setDone(true);
return;
}
//for GeneralCodeFormatterTest
if (myJavaIndentOptions == null) {
myJavaIndentOptions = mySettings.getIndentOptions(StdFileTypes.JAVA);
}
myProgressCallback.beforeApplyingFormatChanges(myBlocksToModify);
final int blocksToModifyCount = myBlocksToModify.size();
final boolean bulkReformat = blocksToModifyCount > 50;
DocumentEx updatedDocument = bulkReformat ? getAffectedDocument(myModel) : null;
if (updatedDocument != null) {
updatedDocument.setInBulkUpdate(true);
myResetBulkUpdateState = true;
}
if (blocksToModifyCount > BULK_REPLACE_OPTIMIZATION_CRITERIA
&& applyChangesAtRewriteMode(myBlocksToModify, myModel, myDefaultIndentOption))
{
setDone(true);
}
}
@Override
protected void doIteration() {
LeafBlockWrapper blockWrapper = myBlocksToModify.get(myIndex);
myShift = replaceWhiteSpace(
myModel,
blockWrapper,
myShift,
blockWrapper.getWhiteSpace().generateWhiteSpace(getIndentOptionsToUse(blockWrapper, myDefaultIndentOption)),
myDefaultIndentOption
);
myProgressCallback.afterApplyingChange(blockWrapper);
// block could be gc'd
blockWrapper.getParent().dispose();
blockWrapper.dispose();
myBlocksToModify.set(myIndex, null);
myIndex++;
if (myIndex >= myBlocksToModify.size()) {
setDone(true);
}
}
@Override
protected void setDone(boolean done) {
super.setDone(done);
if (myResetBulkUpdateState) {
DocumentEx document = getAffectedDocument(myModel);
if (document != null) {
document.setInBulkUpdate(false);
myResetBulkUpdateState = false;
}
}
if (done) {
myModel.commitChanges();
}
}
@Override
public void stop() {
if (myIndex > 0) {
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
myModel.commitChanges();
}
});
}
}
}
}