diff --git a/platform/lang-impl/src/com/intellij/formatting/BulkChangesMerger.java b/platform/lang-impl/src/com/intellij/formatting/BulkChangesMerger.java
new file mode 100644
index 000000000000..239b518ffc09
--- /dev/null
+++ b/platform/lang-impl/src/com/intellij/formatting/BulkChangesMerger.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2010 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.openapi.editor.TextChange;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Encapsulates logic of merging set of changes into particular text.
+ *
+ * Thread-safe.
+ *
+ * @author Denis Zhdanov
+ * @since 12/22/10 12:02 PM
+ */
+public class BulkChangesMerger {
+
+ /**
+ * Merges given changes within the given text and returns result.
+ *
+ * @param text text to apply given changes for
+ * @param changes changes to apply to the given text. It's assumed that there are no intersections between them and that they
+ * are sorted by offsets in ascending order
+ * @return merge result
+ */
+ @SuppressWarnings({"MethodMayBeStatic"})
+ public CharSequence merge(@NotNull char[] text, @NotNull List changes) {
+ int newLength = text.length;
+ for (TextChange change : changes) {
+ newLength += change.getText().length() - (change.getEnd() - change.getStart());
+ }
+ char[] data = new char[newLength];
+ int oldEndOffset = text.length;
+ int newEndOffset = data.length;
+ for (int i = changes.size() - 1; i >= 0; i--) {
+ TextChange change = changes.get(i);
+
+ // Copy all unprocessed symbols from initial text that lay after the changed offset.
+ int symbolsToMoveNumber = oldEndOffset - change.getEnd();
+ System.arraycopy(text, change.getEnd(), data, newEndOffset - symbolsToMoveNumber, symbolsToMoveNumber);
+ newEndOffset -= symbolsToMoveNumber;
+
+ // Copy all change symbols.
+ char[] changeSymbols = change.getChars();
+ newEndOffset -= changeSymbols.length;
+ System.arraycopy(changeSymbols, 0, data, newEndOffset, changeSymbols.length);
+ oldEndOffset = change.getStart();
+ }
+
+ if (oldEndOffset > 0) {
+ System.arraycopy(text, 0, data, 0, oldEndOffset);
+ }
+
+ return new String(data);
+ }
+}
diff --git a/platform/lang-impl/src/com/intellij/formatting/BulkReformatListener.java b/platform/lang-impl/src/com/intellij/formatting/BulkReformatListener.java
new file mode 100644
index 000000000000..89123d1d9d06
--- /dev/null
+++ b/platform/lang-impl/src/com/intellij/formatting/BulkReformatListener.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2000-2010 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.ide.bookmarks.Bookmark;
+import com.intellij.ide.bookmarks.BookmarkManager;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.diff.Diff;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * There is a possible case that we understand that formatting introduces big number of changes to the underlying document.
+ * That number may be big enough for that their subsequent appliance is much slower than building resulting text directly
+ * and replacing the whole document text.
+ *
+ * Current class defines a contract for a listener interested in such situations.
+ *
+ * Not thread-safe.
+ *
+ * @author Denis Zhdanov
+ * @since 12/23/10 11:38 AM
+ */
+public class BulkReformatListener {
+
+ private final List myBookmarks = new ArrayList();
+
+ private final Project myProject;
+ private final VirtualFile myVirtualFile;
+
+ public BulkReformatListener(@NotNull Project project, VirtualFile virtualFile) {
+ myProject = project;
+ myVirtualFile = virtualFile;
+ }
+
+ /**
+ * Is expected to be called before bulk processing.
+ *
+ * @param document document which text is about to be reformatted at bulk mode
+ * @param newText formatted text that is about to replace the text at the given document
+ */
+ public void beforeProcessing(@NotNull Document document, @NotNull CharSequence newText) {
+ reset();
+
+ Diff.Change change = Diff.buildChanges(document.getCharsSequence(), newText);
+ if (change == null) {
+ return;
+ }
+ BookmarkManager manager = BookmarkManager.getInstance(myProject);
+ for (Bookmark bookmark : manager.getValidBookmarks()) {
+ if (bookmark.isValid() && bookmark.getDocument() == document) {
+ int newLine = Diff.translateLine(change, bookmark.getLine());
+ if (newLine >= 0) {
+ myBookmarks.add(new BookmarkData(bookmark.getDescription(), bookmark.getLine()));
+ }
+ }
+ }
+ }
+
+ /**
+ * Is expected to be called just after bulk reformat processing.
+ */
+ public void afterProcessing() {
+ if (myBookmarks.isEmpty()) {
+ return;
+ }
+
+ final BookmarkManager manager = BookmarkManager.getInstance(myProject);
+ for (BookmarkData bookmark : myBookmarks) {
+ manager.addTextBookmark(myVirtualFile, bookmark.line, bookmark.description);
+ }
+
+ reset();
+ }
+
+ private void reset() {
+ myBookmarks.clear();
+ }
+
+ private static class BookmarkData {
+ public final String description;
+ public final int line;
+
+ BookmarkData(String description, int line) {
+ this.description = description;
+ this.line = line;
+ }
+ }
+}
diff --git a/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java b/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java
index 1d34fe4bbd07..fd5a9bbddbc4 100644
--- a/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java
+++ b/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java
@@ -18,7 +18,9 @@ package com.intellij.formatting;
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.softwrap.TextChangeImpl;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
@@ -32,7 +34,16 @@ import java.util.*;
class FormatProcessor {
+ /**
+ * 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 static final BulkChangesMerger ourBulkChangesMerger = new BulkChangesMerger();
private LeafBlockWrapper myCurrentBlock;
@@ -74,7 +85,7 @@ class FormatProcessor {
private LeafBlockWrapper myFirstTokenBlock;
private LeafBlockWrapper myLastTokenBlock;
-
+
private SortedMap> myPreviousDependencies =
new TreeMap>(new Comparator() {
public int compare(final TextRange o1, final TextRange o2) {
@@ -91,6 +102,8 @@ class FormatProcessor {
private WhiteSpace myLastWhiteSpace;
private boolean myDisposed;
private CodeStyleSettings.IndentOptions myJavaIndentOptions;
+ private BulkReformatListener myBulkReformatListener;
+
public FormatProcessor(final FormattingDocumentModel docModel,
Block rootBlock,
@@ -198,15 +211,31 @@ class FormatProcessor {
myJavaIndentOptions = javaIndentOptions;
}
- private static void doModify(final List blocksToModify, final FormattingModel model,
+ /**
+ * @param bulkReformatListener new listener to use; null to reset any listener registered before
+ */
+ public void setBulkReformatListener(@Nullable BulkReformatListener bulkReformatListener) {
+ myBulkReformatListener = bulkReformatListener;
+ }
+
+ private void doModify(final List blocksToModify, final FormattingModel model,
CodeStyleSettings.IndentOptions indentOption, CodeStyleSettings.IndentOptions javaOptions) {
final int blocksToModifyCount = blocksToModify.size();
- final boolean bulkReformat = blocksToModifyCount > 50;
- final DocumentEx updatedDocument = bulkReformat ? getAffectedDocument(model) : null;
- if (updatedDocument != null) {
- updatedDocument.setInBulkUpdate(true);
- }
+ DocumentEx updatedDocument = null;
+
try {
+ if (blocksToModifyCount > BULK_REPLACE_OPTIMIZATION_CRITERIA) {
+ if (applyChangesAtBulkMode(blocksToModify, model, indentOption)) {
+ return;
+ }
+ }
+
+ final boolean bulkReformat = blocksToModifyCount > 50;
+ updatedDocument = bulkReformat ? getAffectedDocument(model) : null;
+ if (updatedDocument != null) {
+ updatedDocument.setInBulkUpdate(true);
+ }
+
int shift = 0;
for (int i = 0; i < blocksToModifyCount; ++i) {
final LeafBlockWrapper block = blocksToModify.get(i);
@@ -226,6 +255,54 @@ class FormatProcessor {
}
}
+ /**
+ * Decides whether applying formatter changes should be applied incrementally one-by-one or merge result should be
+ * constructed locally and the whole document text should be replaced. Performs such single bulk change if necessary.
+ *
+ * @param blocksToModify changes introduced by formatter
+ * @param model current formatting model
+ * @param indentOption indent options to use
+ * @return true if given changes are applied to the document (i.e. no further processing is required);
+ * false otherwise
+ * @see BulkReformatListener
+ */
+ private boolean applyChangesAtBulkMode(final List blocksToModify, final FormattingModel model,
+ CodeStyleSettings.IndentOptions indentOption)
+ {
+ FormattingDocumentModel documentModel = model.getDocumentModel();
+ Document document = documentModel.getDocument();
+ if (document == null) {
+ return false;
+ }
+
+ List changes = new ArrayList();
+ for (LeafBlockWrapper block : blocksToModify) {
+ WhiteSpace whiteSpace = block.getWhiteSpace();
+ CharSequence newWs = documentModel.adjustWhiteSpaceIfNecessary(
+ whiteSpace.generateWhiteSpace(indentOption), whiteSpace.getStartOffset(), whiteSpace.getEndOffset()
+ );
+ changes.add(new TextChangeImpl(newWs, whiteSpace.getStartOffset(), whiteSpace.getEndOffset()));
+ }
+ CharSequence mergeResult = ourBulkChangesMerger.merge(document.getChars(), changes);
+ if (myBulkReformatListener != null) {
+ myBulkReformatListener.beforeProcessing(document, mergeResult);
+ }
+ document.replaceString(0, document.getTextLength(), mergeResult);
+ cleanupBlocks(blocksToModify);
+ if (myBulkReformatListener != null) {
+ myBulkReformatListener.afterProcessing();
+ }
+ return true;
+ }
+
+ private static void cleanupBlocks(List blocks) {
+ for (LeafBlockWrapper block : blocks) {
+ block.getParent().dispose();
+ block.dispose();
+ }
+ blocks.clear();
+ }
+
@Nullable
private static DocumentEx getAffectedDocument(final FormattingModel model) {
if (model instanceof DocumentBasedFormattingModel) {
diff --git a/platform/lang-impl/src/com/intellij/formatting/FormatterImpl.java b/platform/lang-impl/src/com/intellij/formatting/FormatterImpl.java
index efdad42f26d1..d464f329c784 100644
--- a/platform/lang-impl/src/com/intellij/formatting/FormatterImpl.java
+++ b/platform/lang-impl/src/com/intellij/formatting/FormatterImpl.java
@@ -19,9 +19,12 @@ package com.intellij.formatting;
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.formatter.DocumentBasedFormattingModel;
import com.intellij.psi.formatter.FormattingDocumentModelImpl;
import com.intellij.psi.formatter.PsiBasedFormattingModel;
import com.intellij.util.IncorrectOperationException;
@@ -131,13 +134,40 @@ public class FormatterImpl extends FormatterEx
FormatTextRanges affectedRanges) throws IncorrectOperationException {
disableFormatting();
try {
- new FormatProcessor(model.getDocumentModel(), model.getRootBlock(), settings, indentOptions, affectedRanges).format(model);
+ FormatProcessor processor =
+ new FormatProcessor(model.getDocumentModel(), model.getRootBlock(), settings, indentOptions, affectedRanges);
+ setupFormatProcessorIfPossible(processor, model);
+ processor.format(model);
} finally {
enableFormatting();
}
}
+ private static void setupFormatProcessorIfPossible(@NotNull FormatProcessor processor, @NotNull FormattingModel model) {
+ if (!(model instanceof DocumentBasedFormattingModel)) {
+ return;
+ }
+
+ DocumentBasedFormattingModel formattingModel = (DocumentBasedFormattingModel)model;
+ Project project = formattingModel.getProject();
+ if (project == null) {
+ return;
+ }
+
+ PsiFile file = formattingModel.getFile();
+ if (file == null) {
+ return;
+ }
+
+ VirtualFile virtualFile = file.getVirtualFile();
+ if (virtualFile == null) {
+ return;
+ }
+
+ processor.setBulkReformatListener(new BulkReformatListener(project, virtualFile));
+ }
+
public void formatWithoutModifications(FormattingDocumentModel model,
Block rootBlock,
CodeStyleSettings settings,
diff --git a/platform/lang-impl/src/com/intellij/psi/formatter/DocumentBasedFormattingModel.java b/platform/lang-impl/src/com/intellij/psi/formatter/DocumentBasedFormattingModel.java
index 64476daa9963..ca0295ebca2d 100644
--- a/platform/lang-impl/src/com/intellij/psi/formatter/DocumentBasedFormattingModel.java
+++ b/platform/lang-impl/src/com/intellij/psi/formatter/DocumentBasedFormattingModel.java
@@ -35,12 +35,13 @@ import org.jetbrains.annotations.Nullable;
* @author lesya
*/
public class DocumentBasedFormattingModel implements FormattingModel {
- private final Block myRootBlock;
+ private final Block myRootBlock;
private final FormattingDocumentModel myDocumentModel;
- private final Document myDocument;
- private final Project myProject;
- private final CodeStyleSettings mySettings;
- private final FileType myFileType;
+ private final Document myDocument;
+ private final Project myProject;
+ private final CodeStyleSettings mySettings;
+ private final FileType myFileType;
+ private final PsiFile myFile;
public DocumentBasedFormattingModel(final Block rootBlock,
final Document document,
@@ -53,6 +54,7 @@ public class DocumentBasedFormattingModel implements FormattingModel {
myProject = project;
mySettings = settings;
myFileType = fileType;
+ myFile = file;
myDocumentModel = new FormattingDocumentModelImpl(document,file);
}
@@ -65,6 +67,7 @@ public class DocumentBasedFormattingModel implements FormattingModel {
myProject = project;
mySettings = settings;
myFileType = fileType;
+ myFile = file;
myDocumentModel = FormattingDocumentModelImpl.createOn(file);
myDocument = myDocumentModel.getDocument();
}
@@ -213,6 +216,10 @@ public class DocumentBasedFormattingModel implements FormattingModel {
return myProject;
}
+ public PsiFile getFile() {
+ return myFile;
+ }
+
@Nullable
public static String mergeWsWithCdataMarker(String whiteSpace, final String s, final int cdataPos) {
final int firstCrInGeneratedWs = whiteSpace.indexOf('\n');
diff --git a/platform/lang-impl/testSrc/com/intellij/formatting/BulkChangesMergerTest.java b/platform/lang-impl/testSrc/com/intellij/formatting/BulkChangesMergerTest.java
new file mode 100644
index 000000000000..78a4f98d2d2f
--- /dev/null
+++ b/platform/lang-impl/testSrc/com/intellij/formatting/BulkChangesMergerTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2000-2010 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.openapi.editor.TextChange;
+import com.intellij.openapi.editor.impl.softwrap.TextChangeImpl;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Denis Zhdanov
+ * @since 12/22/2010
+ */
+public class BulkChangesMergerTest {
+
+ private BulkChangesMerger myMerger;
+
+ @Before
+ public void setUp() {
+ myMerger = new BulkChangesMerger();
+ }
+
+ @Test
+ public void simpleReplaceInTheMiddle() {
+ doTest("abcd", "a123d", c("123", 1, 3));
+ }
+
+ @Test
+ public void disjointInserts() {
+ doTest("abcd", "a1b2c3d45", c("1", 1), c("2", 2), c("3", 3), c("45", 4));
+ }
+
+ private static TextChange c(String text, int offset) {
+ return c(text, offset, offset);
+ }
+
+ private static TextChange c(String text, int start, int end) {
+ return new TextChangeImpl(text, start, end);
+ }
+
+ private void doTest(String initial, String expected, TextChange ... changes) {
+ CharSequence actual = myMerger.merge(initial.toCharArray(), Arrays.asList(changes));
+ assertEquals(expected, actual.toString());
+ }
+}
diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java
index b85f435199ac..7312ed7dc4a8 100644
--- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java
+++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java
@@ -147,7 +147,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
private final Key