/* * 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.application.ApplicationManager; 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.DocumentUtil; 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. * <p/> * Current constant holds minimum number of changes that should trigger such {@code '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<LeafBlockWrapper> 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<LeafBlockWrapper> blocksToModify, @NotNull final FormattingModel model) { FormattingDocumentModel documentModel = model.getDocumentModel(); Document document = documentModel.getDocument(); CaretOffsetUpdater caretOffsetUpdater = new CaretOffsetUpdater(document); DocumentUtil.executeInBulk(document, true, ()->{ List<TextChange> 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); }); caretOffsetUpdater.restoreCaretLocations(); cleanupBlocks(blocksToModify); } 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 List<LeafBlockWrapper> collectBlocksToModify() { List<LeafBlockWrapper> 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) { ApplicationManager.getApplication().invokeAndWait(() -> myModel.commitChanges()); } } }