diff --git a/build/scripts/dist.gant b/build/scripts/dist.gant
index b9b5f6408bb1..2795d8af5abe 100644
--- a/build/scripts/dist.gant
+++ b/build/scripts/dist.gant
@@ -29,12 +29,6 @@ setProperty("paths", new Paths(out))
def paths = new Paths(out)
-def includeFile(String filepath) {
- Script s = groovyShell.parse(new File(filepath))
- s.setBinding(binding)
- s
-}
-
target(compile: "Compile project") {
loadProject()
diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/JavaCompletionContributor.java b/java/java-impl/src/com/intellij/codeInsight/completion/JavaCompletionContributor.java
index fbe329a42c91..ade0d69873f6 100644
--- a/java/java-impl/src/com/intellij/codeInsight/completion/JavaCompletionContributor.java
+++ b/java/java-impl/src/com/intellij/codeInsight/completion/JavaCompletionContributor.java
@@ -330,7 +330,7 @@ public class JavaCompletionContributor extends CompletionContributor {
return false;
}
- if (NameUtil.useMinusculeHumpMatcher) return true;
+ if (NameUtil.isUseMinusculeHumpMatcher()) return true;
final String s = result.getPrefixMatcher().getPrefix();
if (StringUtil.isEmpty(s) || !Character.isUpperCase(s.charAt(0))) return false;
diff --git a/platform/lang-impl/src/com/intellij/application/options/DocumentChangesCollector.java b/platform/lang-impl/src/com/intellij/application/options/DocumentChangesCollector.java
index d078cbff1bad..fc429ae452fa 100644
--- a/platform/lang-impl/src/com/intellij/application/options/DocumentChangesCollector.java
+++ b/platform/lang-impl/src/com/intellij/application/options/DocumentChangesCollector.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2000-2010 JetBrains s.r.o.
+ * Copyright 2000-2011 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.
@@ -19,7 +19,7 @@ import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.TextChange;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
-import com.intellij.openapi.editor.impl.softwrap.TextChangeImpl;
+import com.intellij.openapi.editor.impl.TextChangeImpl;
import gnu.trove.TIntArrayList;
import org.jetbrains.annotations.NotNull;
@@ -190,7 +190,7 @@ public class DocumentChangesCollector implements DocumentListener {
}
private void mergeChangesIfNecessary(DocumentEvent event) {
- // There is a possible case that we had more than scattered change (e.g. (3; 5) and (8; 10)) and current document change affects
+ // There is a possible case that we had more than one scattered change (e.g. (3; 5) and (8; 10)) and current document change affects
// both of them (e.g. remove all symbols from offset (4; 9)). We have two changes then: (3; 4) and (4; 5) and want to merge them
// into a single one.
if (myChanges.size() < 2) {
diff --git a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/AutoHardWrapHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/AutoHardWrapHandler.java
index a1c2d6d2c732..b1458e6434f7 100644
--- a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/AutoHardWrapHandler.java
+++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/AutoHardWrapHandler.java
@@ -25,7 +25,7 @@ import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.actionSystem.EditorActionManager;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
-import com.intellij.openapi.editor.impl.softwrap.TextChangeImpl;
+import com.intellij.openapi.editor.impl.TextChangeImpl;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.psi.codeStyle.CodeStyleSettings;
diff --git a/platform/lang-impl/src/com/intellij/formatting/BulkChangesMerger.java b/platform/lang-impl/src/com/intellij/formatting/BulkChangesMerger.java
deleted file mode 100644
index 5147013a4caa..000000000000
--- a/platform/lang-impl/src/com/intellij/formatting/BulkChangesMerger.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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 textLength interested number of symbols from the given text to use
- * @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, int textLength, @NotNull List changes) {
- int newLength = textLength;
- for (TextChange change : changes) {
- newLength += change.getText().length() - (change.getEnd() - change.getStart());
- }
- char[] data = new char[newLength];
- int oldEndOffset = textLength;
- 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/FormatProcessor.java b/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java
index 6017b2db2664..4870cfef4cbb 100644
--- a/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java
+++ b/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java
@@ -20,7 +20,8 @@ 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.editor.impl.BulkChangesMerger;
+import com.intellij.openapi.editor.impl.TextChangeImpl;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
@@ -44,7 +45,6 @@ class FormatProcessor {
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;
@@ -298,7 +298,7 @@ class FormatProcessor {
);
changes.add(new TextChangeImpl(newWs, whiteSpace.getStartOffset(), whiteSpace.getEndOffset()));
}
- CharSequence mergeResult = ourBulkChangesMerger.merge(document.getChars(), document.getTextLength(), changes);
+ CharSequence mergeResult = BulkChangesMerger.INSTANCE.mergeToCharSequence(document.getChars(), document.getTextLength(), changes);
document.replaceString(0, document.getTextLength(), mergeResult);
cleanupBlocks(blocksToModify);
return true;
diff --git a/platform/lang-impl/testSrc/com/intellij/application/options/DocumentChangesCollectorTest.java b/platform/lang-impl/testSrc/com/intellij/application/options/DocumentChangesCollectorTest.java
index 2003078cc102..de4a35cf2e95 100644
--- a/platform/lang-impl/testSrc/com/intellij/application/options/DocumentChangesCollectorTest.java
+++ b/platform/lang-impl/testSrc/com/intellij/application/options/DocumentChangesCollectorTest.java
@@ -2,8 +2,8 @@ package com.intellij.application.options;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.TextChange;
+import com.intellij.openapi.editor.impl.TextChangeImpl;
import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
-import com.intellij.openapi.editor.impl.softwrap.TextChangeImpl;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.integration.junit4.JUnit4Mockery;
diff --git a/platform/lang-impl/testSrc/com/intellij/formatting/BulkChangesMergerTest.java b/platform/lang-impl/testSrc/com/intellij/formatting/BulkChangesMergerTest.java
index 5e57751803d6..34cfbd154679 100644
--- a/platform/lang-impl/testSrc/com/intellij/formatting/BulkChangesMergerTest.java
+++ b/platform/lang-impl/testSrc/com/intellij/formatting/BulkChangesMergerTest.java
@@ -15,11 +15,15 @@
*/
package com.intellij.formatting;
-import com.intellij.openapi.editor.TextChange;
-import com.intellij.openapi.editor.impl.softwrap.TextChangeImpl;
+import com.intellij.openapi.editor.impl.BulkChangesMerger;
+import com.intellij.openapi.editor.impl.TextChangeImpl;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TestWatchman;
+import org.junit.runners.model.FrameworkMethod;
+import java.lang.annotation.*;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
@@ -30,6 +34,30 @@ import static org.junit.Assert.assertEquals;
*/
public class BulkChangesMergerTest {
+ @Rule
+ public TestWatchman configReader = new TestWatchman() {
+ @Override
+ public void starting(FrameworkMethod method) {
+ Config config = method.getAnnotation(Config.class);
+ if (config != null) {
+ myConfig = config;
+ }
+ else {
+ try {
+ myConfig = BulkChangesMergerTest.class.getMethod("dummy").getAnnotation(Config.class);
+ }
+ catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ };
+
+ @Config
+ public static void dummy() {
+ }
+
+ private Config myConfig;
private BulkChangesMerger myMerger;
@Before
@@ -47,25 +75,87 @@ public class BulkChangesMergerTest {
doTest("abcd", "a1b2c3d45", c("1", 1), c("2", 2), c("3", 3), c("45", 4));
}
+ @Config(initialTextLength = 4)
@Test
public void interestedSymbolsNumberLessThanAvailable() {
- doTest("abcdefg", 4, "a12bc3d", c("12", 1), c("3", 3));
+ doTest("abcdefg", "a12bc3d", c("12", 1), c("3", 3));
}
- private static TextChange c(String text, int offset) {
+ @Config(inplace = true)
+ @Test
+ public void inplaceZeroGroups() {
+ doTest("0123456789", "a2bc4defg8", c("a", 0, 2), c("bc", 3, 4), c("defg", 5, 8), c("", 9, 10));
+ }
+
+ @Config(inplace = true)
+ @Test
+ public void inplaceGrowingGroupsWithLastPositive() {
+ doTest("abcdefghijklmnopqrst", "acABdCjkDEFGHIJKLMpqrst", c("", 1, 2), c("AB", 3), c("C", 4, 9), c("DEFGHIJKLM", 11, 15));
+ }
+
+ @Config(inplace = true)
+ @Test
+ public void inplaceGrowingGroupsWithLastNegative() {
+ doTest("abcdefghijk", "acABdCjk", c("", 1, 2), c("AB", 3), c("C", 4, 9));
+ }
+
+ @Config(inplace = true)
+ @Test
+ public void onlyGrowing() {
+ doTest("0123456789", "0ab2c3defg67hijk9", c("ab", 1, 2), c("c", 3), c("defg", 4, 6), c("hijk", 8, 9));
+ }
+
+ @Config(inplace = true)
+ @Test
+ public void onlyShrinking() {
+ doTest("0123456789", "0a35b9", c("a", 1, 3), c("", 4, 5), c("b", 6, 9));
+ }
+
+ @Config(inplace = true, dataArrayLength = 4)
+ @Test(expected = IllegalArgumentException.class)
+ public void insufficientLengthForInplaceMerge() {
+ doTest("0123", "", c("", 1, 3), c("abc", 4));
+ }
+
+ private static TextChangeImpl c(String text, int offset) {
return c(text, offset, offset);
}
- private static TextChange c(String text, int start, int end) {
+ private static TextChangeImpl c(String text, int start, int end) {
return new TextChangeImpl(text, start, end);
}
- private void doTest(String initial, String expected, TextChange ... changes) {
- doTest(initial, initial.length(), expected, changes);
+ private void doTest(String initial, String expected, TextChangeImpl ... changes) {
+ if (myConfig.inplace()) {
+ int diff = 0;
+ for (TextChangeImpl change : changes) {
+ diff += change.getDiff();
+ }
+ int outputUsefulLength = initial.length() + diff;
+ int dataLength = myConfig.dataArrayLength();
+ if (dataLength < 0) {
+ dataLength = Math.max(outputUsefulLength, initial.length());
+ }
+ char[] data = new char[dataLength];
+ System.arraycopy(initial.toCharArray(), 0, data, 0, initial.length());
+ myMerger.mergeInPlace(data, initial.length(), Arrays.asList(changes));
+ assertEquals(expected, new String(data, 0, outputUsefulLength));
+ }
+ else {
+ int interestedSymbolsNumber = myConfig.initialTextLength();
+ if (interestedSymbolsNumber < 0) {
+ interestedSymbolsNumber = initial.length();
+ }
+ CharSequence actual = myMerger.mergeToCharSequence(initial.toCharArray(), interestedSymbolsNumber, Arrays.asList(changes));
+ assertEquals(expected, actual.toString());
+ }
}
-
- private void doTest(String initial, int interestedInitialSymbolsNumber, String expected, TextChange ... changes) {
- CharSequence actual = myMerger.merge(initial.toCharArray(), interestedInitialSymbolsNumber, Arrays.asList(changes));
- assertEquals(expected, actual.toString());
+
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
+ private @interface Config {
+ boolean inplace() default false;
+ int dataArrayLength() default -1;
+ int initialTextLength() default -1;
}
}
diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/BulkChangesMerger.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/BulkChangesMerger.java
new file mode 100644
index 000000000000..a23ad662b501
--- /dev/null
+++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/BulkChangesMerger.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2000-2011 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.openapi.editor.impl;
+
+import com.intellij.openapi.editor.TextChange;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+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
+ */
+@SuppressWarnings({"MethodMayBeStatic"})
+public class BulkChangesMerger {
+
+ public static final BulkChangesMerger INSTANCE = new BulkChangesMerger();
+
+ /**
+ * Merges given changes within the given text and returns result as a new char sequence.
+ *
+ * @param text text to apply given changes for
+ * @param textLength interested number of symbols from the given text to use
+ * @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
+ */
+ public CharSequence mergeToCharSequence(@NotNull char[] text, int textLength, @NotNull List extends TextChange> changes) {
+ return new String(mergeToCharArray(text, textLength, changes));
+ }
+
+ /**
+ * Merges given changes within the given text and returns result as a new char array.
+ *
+ * @param text text to apply given changes for
+ * @param textLength interested number of symbols from the given text to use
+ * @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
+ */
+ public char[] mergeToCharArray(@NotNull char[] text, int textLength, @NotNull List extends TextChange> changes) {
+ int newLength = textLength;
+ for (TextChange change : changes) {
+ newLength += change.getText().length() - (change.getEnd() - change.getStart());
+ }
+ char[] data = new char[newLength];
+ int oldEndOffset = textLength;
+ 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 data;
+ }
+
+ /**
+ * Allows to perform 'in-place' merge of the given changes to the given array.
+ *
+ * I.e. it's considered that given array contains particular text at [0; length) region and given changes define
+ * offsets against it. It's also assumed that given array length is enough to contain resulting text after applying the changes.
+ *
+ * Example: consider that initial text is '12345' and given changes are 'remove text at [1; 3) interval'
+ * and 'replace text at [4; 5) interval with 'abcde''. Resulting text is '14abcde' then and given array
+ * length should be not less than 7.
+ *
+ * @param data data array
+ * @param length initial text length (without changes)
+ * @param changes change to apply to the target text
+ * @throws IllegalArgumentException if given array is not big enough to contain the resulting text
+ */
+ public void mergeInPlace(@NotNull char[] data, int length, @NotNull List extends TextChangeImpl> changes)
+ throws IllegalArgumentException
+ {
+ // Consider two corner cases:
+ // 1. Every given change increase text length, i.e. change text length is more than changed region length. We can calculate
+ // resulting text length and start merging the changes from the right end then;
+ // 2. Every given change reduces text length, start from the left end then;
+ // The general idea is to group all of the given changes by 'add text'/ 'remove text' criteria and process them sequentially.
+ // Example: let's assume we have the following changes:
+ // 1) replace two symbols with five (diff +3);
+ // 2) replace two symbols by one (diff -1);
+ // 3) replace two symbols by one (diff -1);
+ // 4) replace four symbols by one (diff -3);
+ // 5) replace one symbol by two (diff +2);
+ // 6) replace one symbol by three (diff +2);
+ // Algorithm:
+ // 1. Define the first group of change. First change diff is '+3', hence, iterate all changes until the resulting diff becomes
+ // equal or less to the zero. So, the first four changes conduct the first group. Initial change increased text length, hence,
+ // we process the changes from right to left starting at offset '4-th change start + 1';
+ // 2. Current diff is '-2' (4-th change diff is '-3' and one slot was necessary for previous group completion), so, that means
+ // that we should process the 4-th and 5-th changes as the second group. Initial change direction is negative, hence, we
+ // process them from left to the right;
+ // 3. Process the remaining change;
+ if (changes.isEmpty()) {
+ return;
+ }
+
+ int diff = 0;
+ for (TextChangeImpl change : changes) {
+ diff += change.getDiff();
+ }
+ if (length + diff > data.length) {
+ throw new IllegalArgumentException(String.format(
+ "Can't perform in-place changes merge. Reason: data array is not big enough to hold resulting text. Current size: %d, "
+ + "minimum size: %d", data.length, length + diff
+ ));
+ }
+
+ for (Context context = new Context(changes, data, length, length + diff); !context.isComplete();) {
+ if (!context.startGroup()) {
+ return;
+ }
+ context.endGroup();
+ }
+ }
+
+ private static void copy(@NotNull char[] data, int offset, @NotNull CharSequence text) {
+ for (int i = 0; i < text.length(); i++) {
+ data[i + offset] = text.charAt(i);
+ }
+ }
+
+ private static class Context {
+
+ private final List extends TextChangeImpl> myChanges;
+ private final char[] myData;
+ private final int myInputLength;
+ private final int myOutputLength;
+ private int myDataStartOffset;
+ private int myDataEndOffset;
+ private int myChangeGroupStartIndex;
+ private int myChangeGroupEndIndex;
+ private int myDiff;
+ private int myFirstChangeShift;
+ private int myLastChangeShift;
+
+ Context(@NotNull List extends TextChangeImpl> changes, @NotNull char[] data, int inputLength, int outputLength) {
+ myChanges = changes;
+ myData = data;
+ myInputLength = inputLength;
+ myOutputLength = outputLength;
+ }
+
+ /**
+ * Asks current context to update its state in order to point to the first change in a group.
+ *
+ * @return true if the first change in a group is found; false otherwise
+ */
+ @SuppressWarnings({"ForLoopThatDoesntUseLoopVariable"})
+ public boolean startGroup() {
+ // Define first change that increases or reduces text length.
+ for (boolean first = true; myDiff == 0 && myChangeGroupStartIndex < myChanges.size(); myChangeGroupStartIndex++, first = false) {
+ TextChangeImpl change = myChanges.get(myChangeGroupStartIndex);
+ myDiff = change.getDiff();
+ if (first) {
+ myDiff += myFirstChangeShift;
+ }
+ if (myDiff == 0) {
+ copy(myData, change.getStart() + (first ? myFirstChangeShift : 0), change.getText());
+ }
+ else {
+ myDataStartOffset = change.getStart();
+ if (first) {
+ myDataStartOffset += myFirstChangeShift;
+ }
+ break;
+ }
+ }
+ return myDiff != 0;
+ }
+
+ public void endGroup() {
+ boolean includeEndChange = false;
+ myLastChangeShift = 0;
+ for (myChangeGroupEndIndex = myChangeGroupStartIndex + 1; myChangeGroupEndIndex < myChanges.size(); myChangeGroupEndIndex++) {
+ assert myDiff != 0 : String.format(
+ "Text: '%s', length: %d, changes: %s, change group indices: %d-%d",
+ Arrays.toString(myData), myInputLength, myChanges, myChangeGroupStartIndex, myChangeGroupEndIndex);
+ TextChangeImpl change = myChanges.get(myChangeGroupEndIndex);
+ int newDiff = myDiff + change.getDiff();
+
+ // Changes group results to the zero text length shift.
+ if (newDiff == 0) {
+ myDataEndOffset = change.getEnd();
+ includeEndChange = true;
+ break;
+ }
+
+ // Changes group is not constructed yet.
+ if (!(myDiff > 0 ^ newDiff > 0)) {
+ myDiff = newDiff;
+ continue;
+ }
+
+ // Current change finishes changes group.
+ myDataEndOffset = change.getStart() + myDiff;
+ myLastChangeShift = myDiff;
+ break;
+ }
+
+ if (myChangeGroupEndIndex >= myChanges.size()) {
+ if (myDiff > 0) {
+ processLastPositiveGroup();
+ }
+ else {
+ processLastNegativeGroup();
+ }
+ myChangeGroupStartIndex = myChangeGroupEndIndex = myChanges.size();
+ }
+ else if (myDiff > 0) {
+ processPositiveGroup(includeEndChange);
+ }
+ else {
+ processNegativeGroup(includeEndChange);
+ }
+ myDiff = 0;
+ myChangeGroupStartIndex = myChangeGroupEndIndex;
+ if (includeEndChange) {
+ myChangeGroupStartIndex++;
+ }
+ myFirstChangeShift = myLastChangeShift;
+ }
+
+ /**
+ * Asks to process changes group identified by [{@link #myChangeGroupStartIndex}; {@link #myChangeGroupEndIndex}) where
+ * overall group direction is 'positive' (i.e. it starts from the change that increases text length).
+ *
+ * @param includeEndChange flag that defines if change defined by {@link #myChangeGroupEndIndex} should be processed
+ */
+ private void processPositiveGroup(boolean includeEndChange) {
+ int outputOffset = myDataEndOffset;
+ int prevChangeStart = -1;
+ for (int i = myChangeGroupEndIndex; i >= myChangeGroupStartIndex; i--) {
+ TextChangeImpl change = myChanges.get(i);
+ if (prevChangeStart >= 0) {
+ int length = prevChangeStart - change.getEnd();
+ System.arraycopy(myData, change.getEnd(), myData, outputOffset - length, length);
+ outputOffset -= length;
+ }
+ prevChangeStart = change.getStart();
+ if (i == myChangeGroupEndIndex && !includeEndChange) {
+ continue;
+ }
+ int length = change.getText().length();
+ if (length > 0) {
+ copy(myData, outputOffset - length, change.getText());
+ outputOffset -= length;
+ }
+ }
+ }
+
+ private void processLastPositiveGroup() {
+ int end = myChanges.get(myChanges.size() - 1).getEnd();
+ int length = myInputLength - end;
+ myDataEndOffset = myOutputLength - length;
+ System.arraycopy(myData, end, myData, myDataEndOffset, length);
+ myChangeGroupEndIndex = myChanges.size() - 1;
+ processPositiveGroup(true);
+ }
+
+ private void processNegativeGroup(boolean includeEndChange) {
+ int prevChangeEnd = -1;
+ for (int i = myChangeGroupStartIndex; i <= myChangeGroupEndIndex; i++) {
+ TextChangeImpl change = myChanges.get(i);
+ if (prevChangeEnd >= 0) {
+ int length = change.getStart() - prevChangeEnd;
+ System.arraycopy(myData, prevChangeEnd, myData, myDataStartOffset, length);
+ myDataStartOffset += length;
+ }
+ prevChangeEnd = change.getEnd();
+ if (i == myChangeGroupEndIndex && !includeEndChange) {
+ return;
+ }
+ int length = change.getText().length();
+ if (length > 0) {
+ copy(myData, myDataStartOffset, change.getText());
+ myDataStartOffset += length;
+ }
+ }
+ }
+
+ private void processLastNegativeGroup() {
+ myChangeGroupEndIndex = myChanges.size() - 1;
+ processNegativeGroup(true);
+ int end = myChanges.get(myChangeGroupEndIndex).getEnd();
+ System.arraycopy(myData, end, myData, myDataStartOffset, myInputLength - end);
+ }
+
+ public boolean isComplete() {
+ return myChangeGroupStartIndex >= myChanges.size();
+ }
+ }
+}
diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/CharArray.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/CharArray.java
index d518590475d1..dc8dcbe03320 100644
--- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/CharArray.java
+++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/CharArray.java
@@ -16,27 +16,69 @@
package com.intellij.openapi.editor.impl;
import com.intellij.openapi.editor.event.DocumentEvent;
+import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
import com.intellij.util.LocalTimeCounter;
import com.intellij.util.text.CharArrayCharSequence;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.text.CharSequenceBackedByArray;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference;
+import java.util.List;
/**
* @author cdr
*/
abstract class CharArray implements CharSequenceBackedByArray {
+
+ private static final boolean DISABLE_DEFERRED_PROCESSING = Boolean.getBoolean("idea.document.deny.deferred.changes");
+
+ /**
+ * We can't exclude possibility of situation when 'defer changes' state is {@link #setDeferredChangeMode(boolean) entered}
+ * but not exited, hence, we want to perform automatic flushing if necessary in order to avoid memory leaks. This constant holds
+ * a value that defines that 'automatic flushing' criteria, i.e. every time number of stored deferred changes exceeds this value,
+ * they are automatically flushed.
+ */
+ private static final int MAX_DEFERRED_CHANGES_NUMBER = 10000;
+
+ @NotNull
+ private TextChangesStorage myDeferredChangesStorage;
+
+ private int myStart;
+ /**
+ * This class implements {@link #subSequence(int, int)} by creating object of the same class that partially shares the same
+ * data as the object on which the method is called. So, this field may define interested end offset (if it's non-negative).
+ */
+ private int myEnd = -1;
+
private int myCount = 0;
private CharSequence myOriginalSequence;
private char[] myArray = null;
private SoftReference myStringRef = null; // buffers String value - for not to generate it every time
private int myBufferSize;
+ private int myDeferredShift;
+ private boolean myDeferredChangeMode;
// max chars to hold, bufferSize == 0 means unbounded
CharArray(int bufferSize) {
+ this(bufferSize, new TextChangesStorage(), null, -1, -1);
+ }
+
+ private CharArray(int bufferSize, @NotNull TextChangesStorage deferredChangesStorage, @Nullable char[] data, int start, int end) {
myBufferSize = bufferSize;
- myOriginalSequence = "";
+ myDeferredChangesStorage = deferredChangesStorage;
+ if (data == null) {
+ myOriginalSequence = "";
+ }
+ else {
+ myArray = data;
+ myCount = end - start;
+ }
+ if (start >= 0 && end >= 0) {
+ myStart = start;
+ myEnd = end;
+ }
}
public void setBufferSize(int bufferSize) {
@@ -55,6 +97,14 @@ abstract class CharArray implements CharSequenceBackedByArray {
myArray = null;
myCount = chars.length();
myStringRef = null;
+ if (isSubSequence()) {
+ myDeferredChangesStorage = new TextChangesStorage();
+ myStart = 0;
+ myEnd = -1;
+ }
+ else {
+ myDeferredChangesStorage.clear();
+ }
trimToSize(subj);
}
@@ -62,6 +112,8 @@ abstract class CharArray implements CharSequenceBackedByArray {
int startOffset, int endOffset, CharSequence toDelete, CharSequence newString, long newModificationStamp,
boolean wholeTextReplaced) {
final DocumentEvent event = beforeChangedUpdate(subj, startOffset, toDelete, newString, wholeTextReplaced);
+ startOffset += myStart;
+ endOffset += myStart;
doReplace(startOffset, endOffset, newString);
afterChangedUpdate(event, newModificationStamp);
}
@@ -69,6 +121,11 @@ abstract class CharArray implements CharSequenceBackedByArray {
private void doReplace(int startOffset, int endOffset, CharSequence newString) {
prepareForModification();
+ if (isDeferredChangeMode()) {
+ storeChange(new TextChangeImpl(newString, startOffset, endOffset));
+ return;
+ }
+
int newLength = newString.length();
int oldLength = endOffset - startOffset;
@@ -84,6 +141,8 @@ abstract class CharArray implements CharSequenceBackedByArray {
public void remove(DocumentImpl subj, int startIndex, int endIndex, CharSequence toDelete) {
DocumentEvent event = beforeChangedUpdate(subj, startIndex, toDelete, null, false);
+ startIndex += myStart;
+ endIndex += myStart;
doRemove(startIndex, endIndex);
afterChangedUpdate(event, LocalTimeCounter.currentTime());
}
@@ -94,6 +153,11 @@ abstract class CharArray implements CharSequenceBackedByArray {
}
prepareForModification();
+ if (isDeferredChangeMode()) {
+ storeChange(new TextChangeImpl("", startIndex, endIndex));
+ return;
+ }
+
if (endIndex < myCount) {
System.arraycopy(myArray, endIndex, myArray, startIndex, myCount - endIndex);
}
@@ -102,6 +166,7 @@ abstract class CharArray implements CharSequenceBackedByArray {
public void insert(DocumentImpl subj, CharSequence s, int startIndex) {
DocumentEvent event = beforeChangedUpdate(subj, startIndex, null, s, false);
+ startIndex += myStart;
doInsert(s, startIndex);
afterChangedUpdate(event, LocalTimeCounter.currentTime());
@@ -111,16 +176,35 @@ abstract class CharArray implements CharSequenceBackedByArray {
private void doInsert(final CharSequence s, final int startIndex) {
prepareForModification();
+ if (isDeferredChangeMode()) {
+ storeChange(new TextChangeImpl(s, startIndex));
+ return;
+ }
+
int insertLength = s.length();
myArray = relocateArray(myArray, myCount + insertLength);
if (startIndex < myCount) {
System.arraycopy(myArray, startIndex, myArray, startIndex + insertLength, myCount - startIndex);
}
- CharArrayUtil.getChars(s, myArray,startIndex);
+ CharArrayUtil.getChars(s, myArray, startIndex);
myCount += insertLength;
}
+ /**
+ * Stores given change at collection of deferred changes (merging it with others if necessary) and updates current object
+ * state ({@link #length() length} etc).
+ *
+ * @param change new change to store
+ */
+ private void storeChange(@NotNull TextChangeImpl change) {
+ if (myDeferredChangesStorage.size() >= MAX_DEFERRED_CHANGES_NUMBER) {
+ flushDeferredChanged();
+ }
+ myDeferredChangesStorage.store(change);
+ myDeferredShift += change.getDiff();
+ }
+
private void prepareForModification() {
if (myOriginalSequence != null) {
myArray = new char[myOriginalSequence.length()];
@@ -141,8 +225,11 @@ abstract class CharArray implements CharSequenceBackedByArray {
if (myOriginalSequence != null) {
str = myOriginalSequence.toString();
}
+ else if (!hasDeferredChanges()) {
+ str = new String(myArray, myStart, myCount);
+ }
else {
- str = new String(myArray, 0, myCount);
+ str = substring(0, length()).toString();
}
myStringRef = new SoftReference(str);
}
@@ -150,40 +237,72 @@ abstract class CharArray implements CharSequenceBackedByArray {
}
public final int length() {
- return myCount;
+ return myCount + myDeferredShift;
}
public final char charAt(int i) {
- if (i < 0 || i >= myCount) {
- throw new IndexOutOfBoundsException("Wrong offset: " + i+"; count:"+myCount);
+ if (i < 0 || i >= length()) {
+ throw new IndexOutOfBoundsException("Wrong offset: " + i + "; count:" + length());
}
+ i += myStart;
if (myOriginalSequence != null) return myOriginalSequence.charAt(i);
- return myArray[i];
+ if (hasDeferredChanges()) {
+ return myDeferredChangesStorage.charAt(myArray, i);
+ }
+ else {
+ return myArray[i];
+ }
}
public CharSequence subSequence(int start, int end) {
- if (start == 0 && end == myCount) return this;
+ if (start == 0 && end == length()) return this;
if (myOriginalSequence != null) {
return myOriginalSequence.subSequence(start, end);
}
- return new CharArrayCharSequence(myArray, start, end);
+ if (hasDeferredChanges()) {
+ return new CharArray(myBufferSize, myDeferredChangesStorage, myArray, myStart + start, myStart + end) {
+ @Override
+ protected DocumentEvent beforeChangedUpdate(DocumentImpl subj,
+ int offset,
+ CharSequence oldString,
+ CharSequence newString,
+ boolean wholeTextReplaced) {
+ return new DocumentEventImpl(subj, offset, oldString, newString, LocalTimeCounter.currentTime(), wholeTextReplaced);
+ }
+
+ @Override
+ protected void afterChangedUpdate(DocumentEvent event, long newModificationStamp) {
+ }
+ };
+ }
+ else {
+ // We don't use the same approach as with 'defer changes' mode because the former is the new experimental one and this one
+ // is rather mature, hence, we just minimizes the risks that something is wrong within the new approach.
+ return new CharArrayCharSequence(myArray, start, end);
+ }
}
+ private boolean isSubSequence() {
+ return myEnd >= 0;
+ }
+
public char[] getChars() {
if (myOriginalSequence != null) {
if (myArray == null) {
myArray = CharArrayUtil.fromSequence(myOriginalSequence);
}
}
+ flushDeferredChanged();
return myArray;
}
public void getChars(final char[] dst, final int dstOffset) {
+ flushDeferredChanged();
if (myOriginalSequence != null) {
CharArrayUtil.getChars(myOriginalSequence,dst, dstOffset);
}
else {
- System.arraycopy(myArray, 0, dst, dstOffset, length());
+ System.arraycopy(myArray, myStart, dst, dstOffset, length());
}
}
@@ -192,7 +311,7 @@ abstract class CharArray implements CharSequenceBackedByArray {
if (myOriginalSequence != null) {
return myOriginalSequence.subSequence(start, end);
}
- return new String(myArray, start, end - start);
+ return myDeferredChangesStorage.substring(myArray, start + myStart, end + myStart);
}
private static char[] relocateArray(char[] array, int index) {
@@ -213,9 +332,82 @@ abstract class CharArray implements CharSequenceBackedByArray {
}
private void trimToSize(DocumentImpl subj) {
- if (myBufferSize != 0 && myCount > myBufferSize) {
+ if (myBufferSize != 0 && length() > myBufferSize) {
+ flushDeferredChanged();
// make a copy
- remove(subj,0, myCount - myBufferSize, getCharArray().subSequence(0, myCount - myBufferSize).toString());
+ remove(subj, 0, myCount - myBufferSize, getCharArray().subSequence(0, myCount - myBufferSize).toString());
}
}
+
+ /**
+ * @return true if this object is at {@link #setDeferredChangeMode(boolean) defer changes} mode;
+ * false otherwise
+ */
+ public boolean isDeferredChangeMode() {
+ return !DISABLE_DEFERRED_PROCESSING && myDeferredChangeMode;
+ }
+
+ public boolean hasDeferredChanges() {
+ return !myDeferredChangesStorage.isEmpty();
+ }
+
+ /**
+ * There is a possible case that client of this class wants to perform great number of modifications in a short amount of time
+ * (e.g. end-user performs formatting of the document backed by the object of the current class). It may result in significant
+ * performance degradation is the changes are performed one by one (every time the change is applied tail content is shifted to
+ * the left or right). So, we may want to optimize that by avoiding actual array modification until information about
+ * all target changes is provided and perform array data moves only after that.
+ *
+ * This method allows to define that 'defer changes' mode usages, i.e. expected usage pattern is as follows:
+ *
+ *
+ * -
+ * Client of this class enters
'defer changes' mode (calls this method with 'true' argument).
+ * That means that all subsequent changes will not actually modify backed array data and will be stored separately;
+ *
+ * -
+ * Number of target changes are applied to the current object via standard API
+ * ({@link #insert(DocumentImpl, CharSequence, int) insert},
+ * {@link #remove(DocumentImpl, int, int, CharSequence) remove} and
+ * {@link #replace(DocumentImpl, int, int, CharSequence, CharSequence, long, boolean) replace});
+ *
+ * -
+ * Client of this class indicates that
'massive change time' is over by calling this method with 'false'
+ * argument. That flushes all deferred changes (if any) to the backed data array and makes every subsequent change to
+ * be immediate flushed to the backed array;
+ *
+ *
+ *
+ *
+ * Note: we can't exclude possibility that 'defer changes' mode is started but inadvertently not ended
+ * (due to programming error, unexpected exception etc). Hence, this class is free to automatically end
+ * 'defer changes' mode when necessary in order to avoid memory leak with infinite deferred changes storing.
+ *
+ * @param deferredChangeMode flag that defines if 'defer changes' mode should be used by the current object
+ */
+ public void setDeferredChangeMode(boolean deferredChangeMode) {
+ myDeferredChangeMode = deferredChangeMode;
+ if (!deferredChangeMode) {
+ flushDeferredChanged();
+ }
+ }
+
+ private void flushDeferredChanged() {
+ List changes = myDeferredChangesStorage.getChanges();
+ if (changes.isEmpty()) {
+ return;
+ }
+
+ BulkChangesMerger changesMerger = BulkChangesMerger.INSTANCE;
+ if (myArray.length < length()) {
+ myArray = changesMerger.mergeToCharArray(myArray, myCount, changes);
+ }
+ else {
+ changesMerger.mergeInPlace(myArray, myCount, changes);
+ }
+
+ myCount += myDeferredShift;
+ myDeferredShift = 0;
+ myDeferredChangesStorage.clear();
+ }
}
diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java
index 838d0b5770f9..2f33d1526d83 100644
--- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java
+++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java
@@ -719,12 +719,12 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
}
public final void setInBulkUpdate(boolean value) {
+ myDoingBulkUpdate = value;
+ myText.setDeferredChangeMode(value);
if (value) {
- myDoingBulkUpdate = true;
getPublisher().updateStarted(this);
}
else {
- myDoingBulkUpdate = false;
getPublisher().updateFinished(this);
normalizeRangeMarkers();
}
diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/TextChangeImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/TextChangeImpl.java
similarity index 99%
rename from platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/TextChangeImpl.java
rename to platform/platform-impl/src/com/intellij/openapi/editor/impl/TextChangeImpl.java
index edac41e22bf8..a0c998895949 100644
--- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/TextChangeImpl.java
+++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/TextChangeImpl.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.intellij.openapi.editor.impl.softwrap;
+package com.intellij.openapi.editor.impl;
import com.intellij.openapi.editor.TextChange;
import com.intellij.openapi.util.text.StringUtil;
diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/TextChangesStorage.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/TextChangesStorage.java
new file mode 100644
index 000000000000..9fb79784d6a9
--- /dev/null
+++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/TextChangesStorage.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright 2000-2011 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.openapi.editor.impl;
+
+import com.intellij.openapi.editor.TextChange;
+import com.intellij.util.text.CharArrayUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Allows to store and retrieve {@link TextChange} objects assuming that they are applied to the same text.
+ *
+ * Provides ability to automatic merging them if necessary.
+ *
+ * Not thread-safe.
+ *
+ * @author Denis Zhdanov
+ * @since 3/2/11 11:55 AM
+ */
+public class TextChangesStorage {
+
+ private final List myChanges = new ArrayList();
+
+ /**
+ * @return list of changes stored previously via {@link #store(TextChange)}. Note that the changes offsets relate to initial
+ * text and that returned list is sorted by start offset in ascending order
+ * @see #store(TextChange)
+ */
+ @NotNull
+ public List getChanges() {
+ List result = new ArrayList();
+ for (ChangeEntry changeEntry : myChanges) {
+ result.add(changeEntry.change);
+ }
+ return result;
+ }
+
+ /**
+ * Allows to ask the storage for the list of changes that have intersections with the target text range (identified by the given
+ * arguments).
+ *
+ * @param start target range start offset (inclusive)
+ * @param end target range end offset (exclusive)
+ * @return list that contains all registered changes that have intersections with the target text range
+ */
+ @NotNull
+ public List extends TextChange> getChanges(int start, int end) {
+ assert start <= end;
+
+ int changeStartIndex = getChangeIndex(start);
+ if (changeStartIndex < 0) {
+ changeStartIndex = -changeStartIndex - 1;
+ }
+ if (changeStartIndex >= myChanges.size()) {
+ return Collections.emptyList();
+ }
+
+ int changeEndIndex = getChangeIndex(end);
+ boolean endInclusive = true;
+ if (changeEndIndex < 0) {
+ changeEndIndex = -changeEndIndex - 1;
+ endInclusive = false;
+ }
+
+ List result = null;
+ for (int i = changeStartIndex; i <= changeEndIndex; i++) {
+ if (!endInclusive && i == changeEndIndex) {
+ break;
+ }
+ if (result == null) {
+ result = new ArrayList();
+ }
+ result.add(myChanges.get(i).change);
+ }
+ return result == null ? Collections.emptyList() : result;
+ }
+
+ public boolean isEmpty() {
+ return myChanges.isEmpty();
+ }
+
+ public void clear() {
+ myChanges.clear();
+ }
+
+ public int size() {
+ return myChanges.size();
+ }
+
+ /**
+ * Store given change merging it with previously stored ones if necessary.
+ *
+ * Note: it's assumed that given change offsets are related to the current state of the text ('client text'),
+ * i.e. with all stored changes applied to it. Example:
+ *
+ * - Say, we have initial text
'12345';
+ * -
+ * Suppose the change
'replace text at [2; 3) range with 'ABC'' is applied to it (stored at the current object).
+ * End-users see the text '12ABC45' now;
+ *
+ * -
+ * This method is called with change like
'replace text at [1; 6) range with 'XY''. Change range is assumed to
+ * be related to the text visible to end-user, not initial one ('12ABC45', not '12345').
+ * I.e. the user will see text '1XY5' now;
+ *
+ *
+ *
+ * @param change change to store
+ */
+ public void store(@NotNull TextChange change) {
+ if (myChanges.isEmpty()) {
+ myChanges.add(new ChangeEntry(new TextChangeImpl(change.getText(), change.getStart(), change.getEnd()), change.getStart()));
+ return;
+ }
+
+ // There is a big chance that the document is processed sequentially from start to end, hence, it makes sense
+ // to check if given change lays beyond other registered changes and register it quickly in case of success.
+ ChangeEntry last = myChanges.get(myChanges.size() - 1);
+ if (last.clientStartOffset + last.change.getText().length() < change.getStart()) {
+ int clientShift = last.clientStartOffset - last.change.getStart() + last.change.getDiff();
+ myChanges.add(new ChangeEntry(
+ new TextChangeImpl(change.getText(), change.getStart() - clientShift, change.getEnd() - clientShift),
+ change.getStart()
+ ));
+ return;
+ }
+
+ int insertionIndex = doStore(change);
+ if (insertionIndex < 0) {
+ return;
+ }
+ mergeIfNecessary(insertionIndex);
+ }
+
+ /**
+ * Stores given change at the current storage and returns its index at {@link #myChanges changes collection} (if any).
+ *
+ * @param change change to store
+ * @return non-negative value that indicates index under which given change is stored at the
+ * {@link #myChanges changes collection}; negative value if given change only modifies sub-range of
+ * already registered range
+ */
+ @SuppressWarnings({"AssignmentToForLoopParameter"})
+ private int doStore(@NotNull TextChange change) {
+ int insertionIndex = 0;
+ int newChangeStart = change.getStart();
+ int newChangeEnd = change.getEnd();
+ int storedChangeStart = getChangeIndex(change.getStart());
+ int clientShift = 0;
+
+ if (storedChangeStart < 0) {
+ storedChangeStart = -storedChangeStart - 1;
+ if (storedChangeStart >= myChanges.size()) {
+ if (storedChangeStart > 0 && storedChangeStart <= myChanges.size()) {
+ ChangeEntry changeEntry = myChanges.get(storedChangeStart - 1);
+ clientShift = changeEntry.clientStartOffset - changeEntry.change.getStart() + changeEntry.change.getDiff();
+ }
+ }
+
+ if (storedChangeStart >= myChanges.size()) {
+ myChanges.add(new ChangeEntry(
+ new TextChangeImpl(change.getText(), change.getStart() - clientShift, change.getEnd() - clientShift),
+ change.getStart()
+ ));
+ return storedChangeStart;
+ }
+ }
+ else {
+ ChangeEntry changeEntry = myChanges.get(storedChangeStart);
+ clientShift = changeEntry.clientStartOffset - changeEntry.change.getStart();
+ }
+
+ for (int i = storedChangeStart; i < myChanges.size(); i++) {
+ ChangeEntry changeEntry = myChanges.get(i);
+ int storedClientStart = changeEntry.change.getStart() + clientShift;
+ CharSequence storedText = changeEntry.change.getText();
+ int storedClientEnd = storedClientStart + storedText.length();
+
+ // Stored change lays before the new one.
+ if (storedClientEnd <= newChangeStart) {
+ clientShift += changeEntry.change.getDiff();
+ insertionIndex = i + 1;
+ continue;
+ }
+
+ // We know that given change and stored change have intersections if control flow reaches this place.
+
+ // Check if given change target sub-range of the stored one
+ if (storedClientStart <= newChangeStart && storedClientEnd >= newChangeEnd) {
+ StringBuilder adjustedText = new StringBuilder();
+ if (storedClientStart < newChangeStart) {
+ adjustedText.append(storedText.subSequence(0, newChangeStart - storedClientStart));
+ }
+ adjustedText.append(change.getText());
+ if (storedClientEnd > newChangeEnd) {
+ adjustedText.append(storedText.subSequence(newChangeEnd - storedClientStart, storedText.length()));
+ }
+
+ if (adjustedText.length() == 0 && changeEntry.change.getStart() == changeEntry.change.getEnd()) {
+ myChanges.remove(i);
+ insertionIndex = -1;
+ break;
+ }
+
+ TextChangeImpl adjusted = new TextChangeImpl(adjustedText, changeEntry.change.getStart(), changeEntry.change.getEnd());
+ myChanges.set(i, new ChangeEntry(adjusted, adjusted.getStart()));
+ insertionIndex = -1;
+ break;
+ }
+
+ // Check if given change completely contains stored change range.
+ if (newChangeStart <= storedClientStart && newChangeEnd >= storedClientEnd) {
+ myChanges.remove(i);
+ insertionIndex = i;
+ newChangeEnd -= changeEntry.change.getText().length();
+ i--;
+ continue;
+ }
+
+ // Check if given change intersects stored change range from the left.
+ if (newChangeStart <= storedClientStart && newChangeEnd < storedClientEnd) {
+ int numberOfStoredChangeSymbolsToRemove = newChangeEnd - storedClientStart;
+ CharSequence adjustedText = storedText.subSequence(numberOfStoredChangeSymbolsToRemove, storedText.length());
+ changeEntry.change = new TextChangeImpl(adjustedText, changeEntry.change.getStart(), changeEntry.change.getEnd());
+ newChangeEnd -= numberOfStoredChangeSymbolsToRemove;
+ insertionIndex = i;
+ continue;
+ }
+
+ // Check if given change intersects stored change range from the right.
+ if (newChangeStart < storedClientEnd && newChangeEnd > storedClientEnd) {
+ CharSequence adjustedText = storedText.subSequence(0, newChangeStart - storedClientStart);
+ TextChangeImpl adjusted = new TextChangeImpl(adjustedText, changeEntry.change.getStart(), changeEntry.change.getEnd());
+ myChanges.set(i, new ChangeEntry(adjusted, adjusted.getStart()));
+ clientShift += adjusted.getDiff();
+ newChangeEnd -= storedClientEnd - newChangeStart;
+ insertionIndex = i + 1;
+ }
+ }
+
+ if (insertionIndex >= 0) {
+ myChanges.add(insertionIndex, new ChangeEntry(
+ new TextChangeImpl(change.getText(), newChangeStart - clientShift, newChangeEnd - clientShift),
+ change.getStart()
+ ));
+ }
+
+ return insertionIndex;
+ }
+
+ /**
+ * Merges if necessary change stored at {@link #myChanges changes collection} at the given index with adjacent changes.
+ *
+ * @param insertionIndex index of the change that can potentially be merged with adjacent changes
+ */
+ private void mergeIfNecessary(int insertionIndex) {
+ // Merge with previous if necessary.
+ ChangeEntry toMerge = myChanges.get(insertionIndex);
+ if (insertionIndex > 0) {
+ ChangeEntry left = myChanges.get(insertionIndex - 1);
+ if (left.change.getEnd() == toMerge.change.getStart()) {
+ String text = left.change.getText().toString() + toMerge.change.getText();
+ left.change = new TextChangeImpl(text, left.change.getStart(), toMerge.change.getEnd());
+ myChanges.remove(insertionIndex);
+ insertionIndex--;
+ }
+ }
+
+ // Merge with next if necessary.
+ toMerge = myChanges.get(insertionIndex);
+ if (insertionIndex < myChanges.size() - 1) {
+ ChangeEntry right = myChanges.get(insertionIndex + 1);
+ if (toMerge.change.getEnd() == right.change.getStart()) {
+ String text = toMerge.change.getText().toString() + right.change.getText();
+ toMerge.change = new TextChangeImpl(text, toMerge.change.getStart(), right.change.getEnd());
+ myChanges.remove(insertionIndex + 1);
+ }
+ }
+ }
+
+ /**
+ * Allows to retrieve character for the given index assuming that it should be resolved against 'client text', i.e. the text contained
+ * at the given original char sequence with all {@link #myChanges registered changes} applied to it.
+ *
+ * Example:
+ *
+ *
+ * - Consider that original text is
'01234';
+ * -
+ * Consider that two changes are registered:
'insert text 'a' at index 1' and
+ * 'insert text 'bc' at index 3';
+ *
+ * 'client text' now is '0a12bc34';
+ * - This method is called with index '5' - symbol 'c' is returned;
+ *
+ *
+ *
+ * @param originalData original text to which {@link #myChanges registered changes} are applied
+ * @param index target symbol index (is assumed to be 'client text' index)
+ * @return 'client text' symbol at the given index
+ */
+ public char charAt(@NotNull char[] originalData, int index) {
+ int changeIndex = getChangeIndex(index);
+ if (changeIndex >= 0) {
+ // Target char is contained at the stored change text
+ ChangeEntry changeEntry = myChanges.get(changeIndex);
+ if (changeEntry.change.getText().length() > index - changeEntry.clientStartOffset) {
+ return changeEntry.change.getText().charAt(index - changeEntry.clientStartOffset);
+ }
+ else {
+ int originalArrayIndex = index - (changeEntry.clientStartOffset - changeEntry.change.getStart() + changeEntry.change.getDiff());
+ return originalData[originalArrayIndex];
+ }
+ }
+ else {
+ int clientShift = 0;
+ changeIndex = -changeIndex - 1;
+ if (changeIndex > 0 && changeIndex <= myChanges.size()) {
+ ChangeEntry changeEntry = myChanges.get(changeIndex - 1);
+ clientShift = changeEntry.clientStartOffset - changeEntry.change.getStart() + changeEntry.change.getDiff();
+ }
+ return originalData[index - clientShift];
+ }
+ }
+
+ /**
+ * Allows to build substring of the client text with its changes registered within the current storage.
+ *
+ * @param originalData original text to which {@link #myChanges registered changes} are applied
+ * @param start target substring start offset (against the 'client text'; inclusive)
+ * @param end target substring end offset (against the 'client text'; exclusive)
+ * @return substring for the given text range
+ */
+ public CharSequence substring(@NotNull char[] originalData, int start, int end) {
+ if (myChanges.isEmpty()) {
+ return new String(originalData, start, end - start);
+ }
+ if (end == start) {
+ return "";
+ }
+
+ int startChangeIndex = getChangeIndex(start);
+ int endChangeIndex = getChangeIndex(end);
+
+ boolean substringAffectedByChanges = startChangeIndex >= 0 || endChangeIndex >= 0 || startChangeIndex != endChangeIndex;
+ int clientShift = 0;
+ int originalStart = 0;
+ if (startChangeIndex < 0) {
+ startChangeIndex = -startChangeIndex - 1;
+ if (startChangeIndex > 0 && startChangeIndex <= myChanges.size()) {
+ ChangeEntry changeEntry = myChanges.get(startChangeIndex - 1);
+ clientShift = changeEntry.clientStartOffset - changeEntry.change.getStart() + changeEntry.change.getDiff();
+ originalStart = changeEntry.change.getEnd();
+ }
+ }
+ else {
+ ChangeEntry changeEntry = myChanges.get(startChangeIndex);
+ clientShift = changeEntry.clientStartOffset - changeEntry.change.getStart();
+ }
+
+ if (!substringAffectedByChanges) {
+ return new String(originalData, start - clientShift, end - start);
+ }
+
+ char[] data = new char[end - start];
+ int outputOffset = 0;
+ for (int i = startChangeIndex; i < myChanges.size() && outputOffset < data.length; i++) {
+ ChangeEntry changeEntry = myChanges.get(i);
+ int clientStart = changeEntry.clientStartOffset;
+ if (clientStart >= end) {
+ if (i == startChangeIndex) {
+ return new String(originalData, start - clientShift, end - start);
+ }
+ System.arraycopy(originalData, originalStart, data, outputOffset, data.length - outputOffset);
+ break;
+ }
+ int clientEnd = clientStart + changeEntry.change.getText().length();
+ if (clientEnd > start) {
+ if (clientStart > start) {
+ int length = Math.min(clientStart - start, changeEntry.change.getStart() - originalStart);
+ length = Math.min(length, data.length - outputOffset);
+ System.arraycopy(originalData, changeEntry.change.getStart() - length, data, outputOffset, length);
+ outputOffset += length;
+ if (outputOffset >= data.length) {
+ break;
+ }
+ }
+ if (end >= clientStart && clientStart < clientEnd) {
+ int changeTextStartOffset = start <= clientStart ? 0 : start - clientStart;
+ int length = Math.min(clientEnd, end) - Math.max(clientStart, start);
+ CharArrayUtil.getChars(changeEntry.change.getText(), data, changeTextStartOffset, outputOffset, length);
+ outputOffset += length;
+ }
+ }
+ originalStart = changeEntry.change.getEnd();
+ }
+
+ if (outputOffset < data.length) {
+ System.arraycopy(originalData, originalStart, data, outputOffset, data.length - outputOffset);
+ }
+ return new String(data);
+ }
+
+ /**
+ * Allows to find index of the change that contains given offset (assuming that it is used against 'client text')
+ * or index of the first change that lays after the given offset.
+ *
+ * @param clientOffset target offset against the 'client text'
+ * @return non-negative value that defines index of the stored change that contains given client offset;
+ * negative value that indicates index of the first change that lays beyond the given offset and
+ * is calculated by by '-returned_index - 1' formula
+ */
+ private int getChangeIndex(int clientOffset) {
+ if (myChanges.isEmpty()) {
+ return -1;
+ }
+
+ int start = 0;
+ int end = myChanges.size() - 1;
+
+ // We inline binary search here because profiling indicates that it becomes bottleneck to use Collections.binarySearch().
+ while (start <= end) {
+ int i = (end + start) >>> 1;
+ ChangeEntry changeEntry = myChanges.get(i);
+ if (changeEntry.clientStartOffset > clientOffset) {
+ end = i - 1;
+ continue;
+ }
+ if (changeEntry.clientStartOffset + changeEntry.change.getText().length() < clientOffset) {
+ start = i + 1;
+ continue;
+ }
+ return i;
+ }
+
+ return -(start + 1);
+ }
+
+ /**
+ * Utility class that contains target {@link TextChangeImpl document change} and auxiliary information associated with it.
+ */
+ private static class ChangeEntry {
+
+ /** Target change. */
+ public TextChangeImpl change;
+
+ /**
+ * Offset of the target change start at the 'client text'.
+ */
+ public int clientStartOffset;
+
+ ChangeEntry(TextChangeImpl change, int clientStartOffset) {
+ this.change = change;
+ this.clientStartOffset = clientStartOffset;
+ }
+ }
+}
diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapImpl.java
index c422fd9c0303..d420544bde71 100644
--- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapImpl.java
+++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapImpl.java
@@ -16,6 +16,7 @@
package com.intellij.openapi.editor.impl.softwrap;
import com.intellij.openapi.editor.SoftWrap;
+import com.intellij.openapi.editor.impl.TextChangeImpl;
import org.jetbrains.annotations.NotNull;
/**
diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/mapping/SoftWrapApplianceManager.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/mapping/SoftWrapApplianceManager.java
index 679cb221a65e..168866225cb3 100644
--- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/mapping/SoftWrapApplianceManager.java
+++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/mapping/SoftWrapApplianceManager.java
@@ -25,6 +25,7 @@ import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.impl.EditorTextRepresentationHelper;
import com.intellij.openapi.editor.impl.FontInfo;
import com.intellij.openapi.editor.impl.IterationState;
+import com.intellij.openapi.editor.impl.TextChangeImpl;
import com.intellij.openapi.editor.impl.softwrap.*;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.util.text.StringUtil;
diff --git a/platform/platform-impl/src/com/intellij/ui/SpeedSearchBase.java b/platform/platform-impl/src/com/intellij/ui/SpeedSearchBase.java
index ae7ec1fc4582..758eb022556d 100644
--- a/platform/platform-impl/src/com/intellij/ui/SpeedSearchBase.java
+++ b/platform/platform-impl/src/com/intellij/ui/SpeedSearchBase.java
@@ -104,7 +104,7 @@ public abstract class SpeedSearchBase extends SpeedSear
if (!isPopupActive()) return null;
final SpeedSearchComparator comparator = getComparator();
final String recentSearchText = comparator.getRecentSearchText();
- return recentSearchText != null && recentSearchText.length() > 0 && comparator.doCompare(recentSearchText, text) && !NameUtil.useMinusculeHumpMatcher ? comparator.getRecentSearchMatcher() : null;
+ return recentSearchText != null && recentSearchText.length() > 0 && comparator.doCompare(recentSearchText, text) && !NameUtil.isUseMinusculeHumpMatcher() ? comparator.getRecentSearchMatcher() : null;
}
/**
@@ -183,7 +183,7 @@ public abstract class SpeedSearchBase extends SpeedSear
if (myRecentSearchText != null &&
myRecentSearchText.equals(pattern)
) {
- if (NameUtil.useMinusculeHumpMatcher) {
+ if (NameUtil.isUseMinusculeHumpMatcher()) {
return myMinusculeMatcher.matches(text);
}
@@ -202,7 +202,7 @@ public abstract class SpeedSearchBase extends SpeedSear
final Pattern recentSearchPattern = Pattern.compile(buf.toString(), allLowercase ? Pattern.CASE_INSENSITIVE : 0);
myRecentSearchMatcher = recentSearchPattern.matcher(text);
- if (NameUtil.useMinusculeHumpMatcher) {
+ if (NameUtil.isUseMinusculeHumpMatcher()) {
myMinusculeMatcher = new NameUtil.MinusculeMatcher(myShouldMatchFromTheBeginning ? pattern : "*" + pattern);
return myMinusculeMatcher.matches(text);
}
diff --git a/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/CharArrayTest.java b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/CharArrayTest.java
new file mode 100644
index 000000000000..84fe8a9eb3cc
--- /dev/null
+++ b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/CharArrayTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2000-2011 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.openapi.editor.impl;
+
+import static org.junit.Assert.*;
+
+import com.intellij.openapi.editor.event.DocumentEvent;
+import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
+import com.intellij.openapi.util.Pair;
+import com.intellij.util.LocalTimeCounter;
+import com.intellij.util.containers.Stack;
+import org.jetbrains.annotations.NotNull;
+import org.jmock.Expectations;
+import org.jmock.api.Invocation;
+import org.jmock.lib.action.CustomAction;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.Before;
+import org.junit.After;
+import org.junit.rules.TestWatchman;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.jmock.Mockery;
+import org.jmock.lib.legacy.ClassImposteriser;
+import org.junit.runners.model.FrameworkMethod;
+
+import java.lang.annotation.*;
+
+/**
+ * @author Denis Zhdanov
+ * @since 03/01/2011
+ */
+public class CharArrayTest {
+
+ @Rule
+ public TestWatchman configReader = new TestWatchman() {
+ @Override
+ public void starting(FrameworkMethod method) {
+ Config config = method.getAnnotation(Config.class);
+ if (config != null) {
+ myConfig = config;
+ }
+ }
+ };
+
+ private CharArray myArray;
+ private Config myConfig;
+ private Mockery myMockery;
+ private DocumentImpl myDocument;
+
+ @Before
+ public void setUp() {
+ myMockery = new JUnit4Mockery() {{
+ setImposteriser(ClassImposteriser.INSTANCE);
+ }};
+ myDocument = myMockery.mock(DocumentImpl.class);
+
+ myMockery.checking(new Expectations() {{
+ allowing(myDocument).getTextLength(); will(new CustomAction("getTextLength") {
+ @Override
+ public Object invoke(Invocation invocation) throws Throwable {
+ return myArray.length();
+ }
+ });
+ }});
+
+ init(10);
+ if (myConfig != null) {
+ myArray.insert(myDocument, myConfig.text(), 0);
+ myArray.setDeferredChangeMode(myConfig.deferred());
+ }
+ }
+
+ @After
+ public void checkExpectations() {
+ myMockery.assertIsSatisfied();
+ }
+
+ @Config(text = "1234", deferred = true)
+ @Test
+ public void deferredReplace() {
+ replace(1, 3, "abc");
+ checkText("1abc4");
+ assertTrue(myArray.hasDeferredChanges());
+
+ replace(2, 3, "XY");
+ checkText("1aXYc4");
+ assertTrue(myArray.hasDeferredChanges());
+
+ replace(3, 6, "ABC");
+ checkText("1aXABC");
+ assertTrue(myArray.hasDeferredChanges());
+
+ myArray.setDeferredChangeMode(false);
+ checkText("1aXABC");
+ assertFalse(myArray.hasDeferredChanges());
+ }
+
+ private void init(int size) {
+ myArray = new CharArray(size) {
+ @Override
+ protected DocumentEvent beforeChangedUpdate(DocumentImpl subj, int offset, CharSequence oldString, CharSequence newString,
+ boolean wholeTextReplaced)
+ {
+ return new DocumentEventImpl(subj, offset, oldString, newString, LocalTimeCounter.currentTime(), wholeTextReplaced);
+ }
+
+ @Override
+ protected void afterChangedUpdate(DocumentEvent event, long newModificationStamp) {
+ }
+ };
+ }
+
+ private void checkText(@NotNull String expected) {
+ // Test as a whole.
+ assertEquals(expected, myArray.toString());
+ assertEquals(expected.length(), myArray.length());
+
+ // Test 'charAt()'.
+ for (int i = 0; i < expected.length(); i++) {
+ if (expected.charAt(i) != myArray.charAt(i)) {
+ fail(String.format(
+ "Detected incorrect 'charAt()' processing for deferred changes. Text: '%1$s'. Expected to get symbol '%2$c' "
+ + "(numeric value %2$d) at index %3$d but actual symbol is '%4$c' (numeric value %4$d)",
+ expected, (int)expected.charAt(i), i, (int)myArray.charAt(i)));
+ }
+ assertEquals(expected.charAt(i), myArray.charAt(i));
+ }
+
+ // Test 'substring()'.
+ for (int start = 0; start < myArray.length() - 1; start++) {
+ for (int end = start; end < myArray.length(); end++) {
+ if (!expected.substring(start, end).equals(myArray.substring(start, end).toString())) {
+ fail(String.format(
+ "Detected incorrect 'substring()' processing for deferred changes. Text: '%s', expected to get substring '%s' for "
+ + "interval [%d; %d) but got '%s'", expected, expected.substring(start, end), start, end, myArray.substring(start, end)
+ ));
+ }
+ }
+ }
+
+ // Test subSequence().
+ checkSubSequence(expected, myArray, new Stack>());
+ }
+
+ private void checkSubSequence(@NotNull String expected, @NotNull CharSequence actual,
+ @NotNull Stack> history)
+ {
+ assertEquals(expected.length(), actual.length());
+ for (int i = 0; i < expected.length(); i++) {
+ char expectedChar = expected.charAt(i);
+ char actualChar = actual.charAt(i);
+ if (expectedChar != actualChar) {
+ fail(String.format(
+ "Detected incorrect charAt() processing for result of subSequence() with deferred changes. Original text: '%s', "
+ + "actual subSequence text: '%s', index: %d, expected symbol: '%c', actual symbol: '%c', subSequence history: %s",
+ myArray.toString(), expected, i, expectedChar, actualChar, history
+ ));
+ }
+ }
+ if (!expected.equals(actual.toString())) {
+ fail(String.format(
+ "Detected incorrect toString() processing for result of subSequence() with deferred changes. Original text: '%s', "
+ + "expected subSequence text: '%s', actual subSequence text: '%s', subSequence history: %s",
+ myArray.toString(), expected, actual.toString(), history
+ ));
+ }
+ assertEquals(expected, actual.toString());
+ for (int start = 0; start < expected.length(); start++) {
+ for (int end = start; end < expected.length(); end++) {
+ history.push(new Pair(start, end));
+ checkSubSequence(expected.substring(start, end), actual.subSequence(start, end), history);
+ history.pop();
+ }
+ }
+ }
+
+ private void replace(int startOffset, int endOffset, String newText) {
+ myArray.replace(
+ myDocument, startOffset, endOffset, myArray.substring(startOffset, endOffset), newText, LocalTimeCounter.currentTime(),
+ startOffset == 0 && endOffset == myArray.length()
+ );
+ }
+
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
+ private @interface Config {
+ String text() default "";
+ boolean deferred() default false;
+ }
+}
diff --git a/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/TextChangesStorageTest.java b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/TextChangesStorageTest.java
new file mode 100644
index 000000000000..ecf250f536da
--- /dev/null
+++ b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/TextChangesStorageTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2000-2011 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.openapi.editor.impl;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.*;
+
+/**
+ * @author Denis Zhdanov
+ * @since 03/02/2011
+ */
+public class TextChangesStorageTest {
+
+ private TextChangesStorage myStorage;
+
+ @Before
+ public void setUp() {
+ myStorage = new TextChangesStorage();
+ }
+
+ @Test
+ public void clear() {
+ assertTrue(myStorage.isEmpty());
+
+ insert("abc", 2);
+ assertFalse(myStorage.isEmpty());
+ assertEquals(1, myStorage.getChanges().size());
+
+ myStorage.clear();
+ assertTrue(myStorage.isEmpty());
+ assertTrue(myStorage.getChanges().isEmpty());
+ }
+
+ @Test
+ public void singleInsert() {
+ insert("abc", 2);
+ checkChanges(c("abc", 2));
+ }
+
+ @Test
+ public void disconnectedInserts() {
+ insert("abc", 2);
+ insert("def", 6);
+ insert("ghi", 11);
+ checkChanges(c("abc", 2), c("def", 3), c("ghi", 5));
+ }
+
+ @Test
+ public void adjacentInserts() {
+ insert("abc", 2);
+ insert("def", 5);
+ insert("ghi", 8);
+ checkChanges(c("abcdefghi", 2));
+ }
+
+ @Test
+ public void nestedInserts() {
+ insert("abc", 2);
+ insert("XY", 3);
+ insert("1234", 4);
+ checkChanges(c("aX1234Ybc", 2));
+ }
+
+ @Test
+ public void singleDelete() {
+ delete(2, 3);
+ checkChanges(c("", 2, 3));
+ }
+
+ @Test
+ public void disconnectedDeletes() {
+ delete(2, 3);
+ delete(3, 4);
+ delete(5, 6);
+ checkChanges(c("", 2, 3), c("", 4, 5), c("", 7, 8));
+ }
+
+ @Test
+ public void adjacentDeletes() {
+ delete(2, 3);
+ delete(2, 3);
+ delete(2, 3);
+ checkChanges(c("", 2, 5));
+ }
+
+ @Test
+ public void singleReplace() {
+ replace("abc", 3, 4);
+ checkChanges(c("abc", 3, 4));
+ }
+
+ @Test
+ public void disconnectedReplaces() {
+ replace("abc", 3, 4);
+ replace("de", 7, 8);
+ replace("fghi", 10, 11);
+ checkChanges(c("abc", 3, 4), c("de", 5, 6), c("fghi", 7, 8));
+ }
+
+ @Test
+ public void adjacentReplaces() {
+ replace("abc", 3, 4);
+ replace("de", 6, 9);
+ replace("fghi", 8, 9);
+ checkChanges(c("abcdefghi", 3, 8));
+ }
+
+ @Test
+ public void intersectedReplaces() {
+ replace("abc", 3, 4);
+ replace("defg", 5, 6);
+ replace("hi", 8, 11);
+ checkChanges(c("abdefhi", 3, 6));
+ }
+
+ @Test
+ public void intersectedReplacesFromEndToStart() {
+ replace("abcd", 5, 6);
+ replace("ef", 4, 7);
+ replace("g", 1, 5);
+ checkChanges(c("gfcd", 1, 6));
+ }
+
+ @Test
+ public void nestedReplaces() {
+ replace("abcdef", 3, 5);
+ replace("gh", 4, 7);
+ replace("i", 5, 6);
+ checkChanges(c("agief", 3, 5));
+ }
+
+ @Test
+ public void exactMultipleReplace() {
+ replace("abc", 3, 4);
+ replace("cde", 3, 6);
+ replace("fg", 3, 6);
+ checkChanges(c("fg", 3, 4));
+ }
+
+ @Test
+ public void insertAndExactDelete() {
+ insert("abc", 3);
+ delete(3, 6);
+ checkChanges();
+ }
+
+ @Test
+ public void insertAndDeleteInTheMiddle() {
+ insert("abc", 3);
+ delete(4, 6);
+ checkChanges(c("a", 3));
+ }
+
+ @Test
+ public void insertAndWiderDelete() {
+ insert("abc", 3);
+ delete(2, 7);
+ checkChanges(c("", 2, 4));
+ }
+
+ @Test
+ public void insertAndDeleteFromLeft() {
+ insert("abc", 3);
+ delete(2, 5);
+ checkChanges(c("c", 2, 3));
+ }
+
+ @Test
+ public void insertAndDeleteFromRight() {
+ insert("abc", 3);
+ delete(4, 7);
+ checkChanges(c("a", 3, 4));
+ }
+
+ @Test
+ public void disconnectedInsertsAndExactLinkingDelete() {
+ insert("a", 1);
+ insert("bcd", 3);
+ insert("efg", 8);
+ delete(3, 11);
+ checkChanges(c("a", 1), c("", 2, 4));
+ }
+
+ @Test
+ public void disconnectedInsertsAndWiderLinkingDelete() {
+ insert("abc", 3);
+ insert("def", 8);
+ delete(2, 13);
+ checkChanges(c("", 2, 7));
+ }
+
+ @Test
+ public void disconnectedInsertsAndNarrowLinkingDelete() {
+ insert("abc", 3);
+ insert("def", 8);
+ delete(4, 9);
+ checkChanges(c("aef", 3, 5));
+ }
+
+ private void checkChanges(TextChangeImpl ... changes) {
+ assertEquals(asList(changes), myStorage.getChanges());
+ assertEquals(changes.length > 0, !myStorage.isEmpty());
+ if (changes.length <= 0) {
+ return;
+ }
+ int length = changes[changes.length - 1].getEnd();
+ char[] input = new char[length];
+ char c = 'A';
+ for (int i = 0; i < input.length; i++) {
+ input[i] = c++;
+ }
+ char[] output = BulkChangesMerger.INSTANCE.mergeToCharArray(input, input.length, asList(changes));
+
+ // charAt().
+ for (int i = 0; i < output.length; i++) {
+ if (output[i] != myStorage.charAt(input, i)) {
+ fail(String.format(
+ "Detected incorrect charAt() processing. Original text: '%s', changes: %s, index: %d, expected: %c, actual: %c",
+ new String(input), Arrays.asList(changes), i, output[i], myStorage.charAt(input, i)
+ ));
+ }
+ }
+
+ // substring().
+ for (int start = 0; start < output.length; start++) {
+ for( int end = start; end < output.length; end++) {
+ String expected = new String(output, start, end - start);
+ String actual = myStorage.substring(input, start, end).toString();
+ if (!expected.equals(actual)) {
+ fail(String.format(
+ "Detected incorrect substring() processing. Original text: '%s', changes: %s, client text: '%s', range: %d-%d, "
+ + "expected: '%s', actual: '%s'", new String(input), Arrays.asList(changes), new String(output), start, end, expected, actual
+ ));
+ }
+ }
+ }
+ }
+
+ private static TextChangeImpl c(@NotNull String text, int startOffset) {
+ return c(text, startOffset, startOffset);
+ }
+
+ private static TextChangeImpl c(@NotNull String text, int startOffset, int endOffset) {
+ return new TextChangeImpl(text, startOffset, endOffset);
+ }
+
+ private void insert(@NotNull String text, int offset) {
+ myStorage.store(c(text, offset));
+ }
+
+ private void delete(int start, int end) {
+ myStorage.store(c("", start, end));
+ }
+
+ private void replace(@NotNull String text, int start, int end) {
+ myStorage.store(c(text, start, end));
+ }
+}
diff --git a/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/TextChangeImplTest.java b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/TextChangeImplTest.java
index ad118949ac3f..f34051e13e9b 100644
--- a/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/TextChangeImplTest.java
+++ b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/TextChangeImplTest.java
@@ -16,6 +16,7 @@
package com.intellij.openapi.editor.impl.softwrap;
import com.intellij.openapi.editor.TextChange;
+import com.intellij.openapi.editor.impl.TextChangeImpl;
import com.intellij.openapi.util.text.StringUtil;
import org.junit.Test;
diff --git a/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/mapping/CachingSoftWrapDataMapperTest.java b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/mapping/CachingSoftWrapDataMapperTest.java
index eb56c58fe7d2..aa663d182127 100644
--- a/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/mapping/CachingSoftWrapDataMapperTest.java
+++ b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/mapping/CachingSoftWrapDataMapperTest.java
@@ -20,6 +20,7 @@ import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.FoldingModelEx;
import com.intellij.openapi.editor.ex.SoftWrapModelEx;
+import com.intellij.openapi.editor.impl.TextChangeImpl;
import com.intellij.openapi.editor.impl.softwrap.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
diff --git a/platform/platform-resources-en/src/misc/registry.properties b/platform/platform-resources-en/src/misc/registry.properties
index 405a73b0a55b..d5bdd55266cc 100644
--- a/platform/platform-resources-en/src/misc/registry.properties
+++ b/platform/platform-resources-en/src/misc/registry.properties
@@ -94,6 +94,7 @@ debugger.mayBringFrameToFrontOnBreakpoint=true
filesystem.useNative=true
analyze.exceptions.on.the.fly=false
+analyze.exceptions.on.the.fly.description=Automatically analyze clipboard on frame activation, and if there is a stacktrace calls Analyze Stacktrace
compiler.perform.outputs.refresh.on.start=false
compiler.perform.outputs.refresh.on.start.description=Whether to perform initial FS refresh before compilation starts. Need this to detect external changes to output dirs
@@ -119,3 +120,5 @@ navbar.userActivityMergeTime=500
navbar.newpopup=true
inspectionGadgets.telemetry.enabled=false
+minuscule.humps.matching=false
+minuscule.humps.matching.description=Camel Case without holding Shift in Ctrl+N/Ctrl+Shift+N etc
diff --git a/platform/platform-api/src/com/intellij/openapi/ui/CheckBoxWithDescription.java b/platform/util/src/com/intellij/openapi/ui/CheckBoxWithDescription.java
similarity index 100%
rename from platform/platform-api/src/com/intellij/openapi/ui/CheckBoxWithDescription.java
rename to platform/util/src/com/intellij/openapi/ui/CheckBoxWithDescription.java
diff --git a/platform/platform-api/src/com/intellij/openapi/ui/DescriptionLabel.java b/platform/util/src/com/intellij/openapi/ui/DescriptionLabel.java
similarity index 100%
rename from platform/platform-api/src/com/intellij/openapi/ui/DescriptionLabel.java
rename to platform/util/src/com/intellij/openapi/ui/DescriptionLabel.java
diff --git a/platform/platform-api/src/com/intellij/openapi/util/registry/Registry.java b/platform/util/src/com/intellij/openapi/util/registry/Registry.java
similarity index 100%
rename from platform/platform-api/src/com/intellij/openapi/util/registry/Registry.java
rename to platform/util/src/com/intellij/openapi/util/registry/Registry.java
diff --git a/platform/platform-api/src/com/intellij/openapi/util/registry/RegistryValue.java b/platform/util/src/com/intellij/openapi/util/registry/RegistryValue.java
similarity index 100%
rename from platform/platform-api/src/com/intellij/openapi/util/registry/RegistryValue.java
rename to platform/util/src/com/intellij/openapi/util/registry/RegistryValue.java
diff --git a/platform/platform-api/src/com/intellij/openapi/util/registry/RegistryValueListener.java b/platform/util/src/com/intellij/openapi/util/registry/RegistryValueListener.java
similarity index 100%
rename from platform/platform-api/src/com/intellij/openapi/util/registry/RegistryValueListener.java
rename to platform/util/src/com/intellij/openapi/util/registry/RegistryValueListener.java
diff --git a/platform/platform-api/src/com/intellij/openapi/util/registry/ui/RegistryCheckBox.java b/platform/util/src/com/intellij/openapi/util/registry/ui/RegistryCheckBox.java
similarity index 100%
rename from platform/platform-api/src/com/intellij/openapi/util/registry/ui/RegistryCheckBox.java
rename to platform/util/src/com/intellij/openapi/util/registry/ui/RegistryCheckBox.java
diff --git a/platform/util/src/com/intellij/psi/codeStyle/NameUtil.java b/platform/util/src/com/intellij/psi/codeStyle/NameUtil.java
index 653fc14bb630..6711afe7272a 100644
--- a/platform/util/src/com/intellij/psi/codeStyle/NameUtil.java
+++ b/platform/util/src/com/intellij/psi/codeStyle/NameUtil.java
@@ -15,6 +15,7 @@
*/
package com.intellij.psi.codeStyle;
+import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
@@ -39,7 +40,6 @@ public class NameUtil {
}
};
private static final int MAX_LENGTH = 40;
- public static final boolean useMinusculeHumpMatcher = "true".equals(System.getProperty("minuscule.humps.matching"));
private NameUtil() {}
@@ -390,12 +390,13 @@ public class NameUtil {
return buildMatcher(pattern, buildRegexp(pattern, exactPrefixLen, allowToUpper, allowToLower, lowerCaseWords, false));
}
- private static Matcher buildMatcher(final String pattern, String regexp) {
- if (useMinusculeHumpMatcher) {
- return new MinusculeMatcher(pattern);
- }
+ public static boolean isUseMinusculeHumpMatcher() {
+ return Registry.is("minuscule.humps.matching");
+ }
- return new OptimizedMatcher(pattern, regexp);
+ private static Matcher buildMatcher(final String pattern, String regexp) {
+ return isUseMinusculeHumpMatcher() ? new MinusculeMatcher(pattern)
+ : new OptimizedMatcher(pattern, regexp);
}
private static class OptimizedMatcher implements Matcher {
diff --git a/platform/util/src/com/intellij/util/containers/ContainerUtil.java b/platform/util/src/com/intellij/util/containers/ContainerUtil.java
index ecfb00ea0c68..e684e0829570 100644
--- a/platform/util/src/com/intellij/util/containers/ContainerUtil.java
+++ b/platform/util/src/com/intellij/util/containers/ContainerUtil.java
@@ -488,7 +488,7 @@ public class ContainerUtil {
}
}
- public static Collection addAll(@NotNull Collection collection, @NotNull T... elements) {
+ public static > C addAll(@NotNull C collection, @NotNull A... elements) {
//noinspection ManualArrayToCollectionCopy
for (T element : elements) {
collection.add(element);
@@ -842,6 +842,11 @@ public class ContainerUtil {
return result.toArray(emptyArray);
}
+ @NotNull
+ public static Set set(T ... items) {
+ return addAll(new HashSet(), items);
+ }
+
public static void addIfNotNull(final T element, @NotNull Collection result) {
if (element != null) {
result.add(element);
diff --git a/platform/util/src/com/intellij/util/text/CharArrayUtil.java b/platform/util/src/com/intellij/util/text/CharArrayUtil.java
index b88de667f4a0..79bc1c4c5e92 100644
--- a/platform/util/src/com/intellij/util/text/CharArrayUtil.java
+++ b/platform/util/src/com/intellij/util/text/CharArrayUtil.java
@@ -31,39 +31,68 @@ public class CharArrayUtil {
private CharArrayUtil() {
}
+ /**
+ * Copies all symbols from the given char sequence to the given array
+ *
+ * @param src source data holder
+ * @param dst output data buffer
+ * @param dstOffset start offset to use within the given output data buffer
+ */
public static void getChars(CharSequence src, char[] dst, int dstOffset) {
getChars(src, dst, dstOffset, src.length());
}
+ /**
+ * Copies necessary number of symbols from the given char sequence start to the given array.
+ *
+ * @param src source data holder
+ * @param dst output data buffer
+ * @param dstOffset start offset to use within the given output data buffer
+ * @param len number of source data symbols to copy to the given buffer
+ */
public static void getChars(CharSequence src, char[] dst, int dstOffset, int len) {
+ getChars(src, dst, 0, dstOffset, len);
+ }
+
+ /**
+ * Copies necessary number of symbols from the given char sequence to the given array.
+ *
+ * @param src source data holder
+ * @param dst output data buffer
+ * @param srcOffset source text offset
+ * @param dstOffset start offset to use within the given output data buffer
+ * @param len number of source data symbols to copy to the given buffer
+ */
+ public static void getChars(CharSequence src, char[] dst, int srcOffset, int dstOffset, int len) {
if (len >= GET_CHARS_THRESHOLD) {
if (src instanceof String) {
- ((String)src).getChars(0, len, dst, dstOffset);
+ ((String)src).getChars(srcOffset, len, dst, dstOffset);
return;
}
else if (src instanceof CharBuffer) {
final CharBuffer buffer = (CharBuffer)src;
final int i = buffer.position();
+ buffer.position(i + srcOffset);
buffer.get(dst, dstOffset, len);
buffer.position(i);
return;
}
else if (src instanceof CharSequenceBackedByArray) {
- ((CharSequenceBackedByArray)src.subSequence(0, len)).getChars(dst, dstOffset);
+ ((CharSequenceBackedByArray)src.subSequence(srcOffset, len)).getChars(dst, dstOffset);
return;
}
else if (src instanceof StringBuffer) {
- ((StringBuffer)src).getChars(0, len, dst, dstOffset);
+ ((StringBuffer)src).getChars(srcOffset, len, dst, dstOffset);
return;
}
else if (src instanceof StringBuilder) {
- ((StringBuilder)src).getChars(0, len, dst, dstOffset);
+ ((StringBuilder)src).getChars(srcOffset, len, dst, dstOffset);
return;
}
}
- for (int i = 0; i < len; i++) {
- dst[i + dstOffset] = src.charAt(i);
+ for (int i = 0, j = srcOffset, max = srcOffset + len; j < max && i < dst.length; i++, j++) {
+ dst[i + dstOffset] = src.charAt(j);
}
}
diff --git a/plugins/InspectionGadgets/src/com/siyeh/ig/bugs/FormatDecode.java b/plugins/InspectionGadgets/src/com/siyeh/ig/bugs/FormatDecode.java
index ec750545ffe5..bfbcdb8550f2 100644
--- a/plugins/InspectionGadgets/src/com/siyeh/ig/bugs/FormatDecode.java
+++ b/plugins/InspectionGadgets/src/com/siyeh/ig/bugs/FormatDecode.java
@@ -17,8 +17,12 @@ package com.siyeh.ig.bugs;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.PsiType;
+import com.intellij.util.containers.ContainerUtil;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -44,6 +48,27 @@ class FormatDecode{
private static final Validator FLOAT_VALIDATOR = new FloatValidator();
+ /**
+ * Holds information about validator replacement rules, i.e. allows to answer if validator of particular type may be
+ * safely replaced by validator of another particular type.
+ *
+ * For example, validator of type {@link AllValidator#type() 'all'} may be safely replaced by validator of any other
+ * type, e.g. {@link DateValidator#type() Date/Time} or {@link CharValidator#type() 'char validator'} may be replaced
+ * by {@link IntValidator#type() 'int validator'} because {@link Formatter java formatter} knows how to
+ * {@link Formatter.FormatSpecifier#printCharacter(Object) print character from integer} etc.
+ *
+ * Generally, current collection holds set of mappings where the key is type of validator that may be safely replaced
+ * by validator of type that is contained at 'values' collection.
+ */
+ private static final Map> REPLACEABLE_VALIDATOR_TYPES = new HashMap>();
+ static {
+ REPLACEABLE_VALIDATOR_TYPES.put(
+ ALL_VALIDATOR.type(),
+ ContainerUtil.set(DATE_VALIDATOR.type(), CHAR_VALIDATOR.type(), INT_VALIDATOR.type(), FLOAT_VALIDATOR.type())
+ );
+ REPLACEABLE_VALIDATOR_TYPES.put(CHAR_VALIDATOR.type(), ContainerUtil.set(INT_VALIDATOR.type()));
+ }
+
public static Validator[] decode(String formatString, int argumentCount){
final ArrayList parameters = new ArrayList();
@@ -108,11 +133,13 @@ class FormatDecode{
int argumentCount){
if(pos < parameters.size()){
final Validator old = parameters.get(pos);
+ Set replaceableTypes = REPLACEABLE_VALIDATOR_TYPES.get(old.type());
+ if (replaceableTypes != null && replaceableTypes.contains(val.type())) {
+ parameters.set(pos, val);
+ }
// it's OK to overwrite ALL with something more specific
// it's OK to ignore overwrite of something else with ALL or itself
- if (old == ALL_VALIDATOR) {
- parameters.set(pos, val);
- } else if (val != ALL_VALIDATOR && val != old) {
+ else if (val != ALL_VALIDATOR && val != old) {
throw new DuplicateFormatFlagsException(
"requires both " + old.type() + " and " + val.type());
}
diff --git a/plugins/InspectionGadgets/test/com/siyeh/igtest/bugs/malformed_format_string/MalformedFormatString.java b/plugins/InspectionGadgets/test/com/siyeh/igtest/bugs/malformed_format_string/MalformedFormatString.java
index d8b0e830f731..4835464100b7 100644
--- a/plugins/InspectionGadgets/test/com/siyeh/igtest/bugs/malformed_format_string/MalformedFormatString.java
+++ b/plugins/InspectionGadgets/test/com/siyeh/igtest/bugs/malformed_format_string/MalformedFormatString.java
@@ -21,6 +21,7 @@ public class MalformedFormatString {
String warn = String.format("%s %s", 1); // this is invalid according to the inspector (correct)
String invalid = String.format("%s %s" + local, 1); // this is valid according to the inspector (INCORRECT!)
String interesting = String.format("%s %s" + "hmm", 1); // this is invalid according to the inspector (correct)
+ String intAsChar = String.format("symbol '%1$c' (numeric value %1$d)", 60); // integer->char conversion is ok (correct)
}
public void outOfMemory() {
diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/completion/GroovyCompletionContributor.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/completion/GroovyCompletionContributor.java
index b86e1ad8fb08..4aec6a74bf94 100644
--- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/completion/GroovyCompletionContributor.java
+++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/completion/GroovyCompletionContributor.java
@@ -454,7 +454,7 @@ public class GroovyCompletionContributor extends CompletionContributor {
});
final String s = result.getPrefixMatcher().getPrefix();
- if (NameUtil.useMinusculeHumpMatcher || !StringUtil.isEmpty(s) && Character.isUpperCase(s.charAt(0))) {
+ if (NameUtil.isUseMinusculeHumpMatcher() || !StringUtil.isEmpty(s) && Character.isUpperCase(s.charAt(0))) {
addAllClasses(parameters, result, inheritors);
}
}
diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/groovydoc/psi/impl/GrDocCommentUtil.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/groovydoc/psi/impl/GrDocCommentUtil.java
index d2c5fd1cc2cb..8eb359156192 100644
--- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/groovydoc/psi/impl/GrDocCommentUtil.java
+++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/groovydoc/psi/impl/GrDocCommentUtil.java
@@ -23,6 +23,8 @@ import org.jetbrains.plugins.groovy.lang.groovydoc.psi.api.GrDocComment;
import org.jetbrains.plugins.groovy.lang.groovydoc.psi.api.GrDocCommentOwner;
import org.jetbrains.plugins.groovy.lang.groovydoc.psi.api.GroovyDocPsiElement;
import org.jetbrains.plugins.groovy.lang.parser.GroovyElementTypes;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariable;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariableDeclaration;
/**
* @author Maxim.Medvedev
@@ -51,7 +53,13 @@ public abstract class GrDocCommentUtil {
@Nullable
public static GrDocComment findDocComment(GrDocCommentOwner owner) {
- PsiElement element = owner.getPrevSibling();
+ PsiElement element;
+ if (owner instanceof GrVariable && owner.getParent() instanceof GrVariableDeclaration) {
+ element = owner.getParent().getPrevSibling();
+ }
+ else {
+ element = owner.getPrevSibling();
+ }
while (true) {
if (element == null) return null;
final ASTNode node = element.getNode();
diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/PsiImplUtil.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/PsiImplUtil.java
index d50aacda0c94..9bf34b111bbd 100644
--- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/PsiImplUtil.java
+++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/PsiImplUtil.java
@@ -31,6 +31,7 @@ import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.MethodSignature;
import com.intellij.psi.util.MethodSignatureUtil;
import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
@@ -103,12 +104,27 @@ public class PsiImplUtil {
if (result != null) return result;
}
- ASTNode oldNode = oldExpr.getNode();
- ASTNode newNode = newExpr.copy().getNode();
- assert newNode != null && parentNode != null;
- parentNode.replaceChild(oldNode, newNode);
-
- return ((GrExpression)newNode.getPsi());
+ //if replace closure argument with expression
+ //we should add the expression in arg list
+ if (oldExpr instanceof GrClosableBlock &&
+ !(newExpr instanceof GrClosableBlock) &&
+ oldParent instanceof GrCall &&
+ ArrayUtil.contains(oldExpr, ((GrCall)oldParent).getClosureArguments())) {
+ final GrClosableBlock[] closureArguments = ((GrCall)oldParent).getClosureArguments();
+ final int i = ArrayUtil.find(closureArguments, oldExpr);
+ GrArgumentList argList = ((GrCall)oldParent).getArgumentList();
+ if (argList.getText().length() == 0) argList = (GrArgumentList)argList.replace(factory.createArgumentList());
+ for (int j = 0; j < i; j++) {
+ argList.add(closureArguments[j]);
+ closureArguments[j].delete();
+ }
+ final GrExpression result = (GrExpression)argList.add(newExpr);
+ oldExpr.delete();
+ return result;
+ }
+ else {
+ return (GrExpression)oldExpr.replace(newExpr);
+ }
}
/**
diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/arguments/GrArgumentListImpl.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/arguments/GrArgumentListImpl.java
index 4ca9e9e0ce91..b1b443108b32 100644
--- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/arguments/GrArgumentListImpl.java
+++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/arguments/GrArgumentListImpl.java
@@ -175,6 +175,11 @@ public class GrArgumentListImpl extends GroovyPsiElementImpl implements GrArgume
return namedArgument;
}
+ @Override
+ public PsiElement add(@NotNull PsiElement element) throws IncorrectOperationException {
+ return addBefore(element, null);
+ }
+
@Override
public PsiElement addBefore(@NotNull PsiElement element, PsiElement anchor) throws IncorrectOperationException {
if (element instanceof GrNamedArgument || element instanceof GrExpression) {
diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/blocks/GrBlockImpl.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/blocks/GrBlockImpl.java
index dfd3f5412c42..f07b0f37dfea 100644
--- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/blocks/GrBlockImpl.java
+++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/blocks/GrBlockImpl.java
@@ -17,6 +17,7 @@
package org.jetbrains.plugins.groovy.lang.psi.impl.statements.blocks;
import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.Key;
import com.intellij.psi.PsiElement;
import com.intellij.psi.ResolveState;
import com.intellij.psi.impl.source.tree.Factory;
@@ -49,7 +50,7 @@ import org.jetbrains.plugins.groovy.lang.resolve.ResolveUtil;
* @author ven
*/
public abstract class GrBlockImpl extends LazyParseablePsiElement implements GrCodeBlock, GrControlFlowOwner {
- private volatile CachedValue myControlFlow = null;
+ private static final Key> CONTROL_FLOW = Key.create("Control flow");
protected GrBlockImpl(@NotNull IElementType type, CharSequence buffer) {
super(type, buffer);
@@ -75,7 +76,7 @@ public abstract class GrBlockImpl extends LazyParseablePsiElement implements GrC
public void subtreeChanged() {
super.subtreeChanged();
- myControlFlow = null;
+ putUserData(CONTROL_FLOW, null);
}
@Override
@@ -96,14 +97,15 @@ public abstract class GrBlockImpl extends LazyParseablePsiElement implements GrC
}
public Instruction[] getControlFlow() {
- CachedValue controlFlow = myControlFlow;
+ CachedValue controlFlow = getUserData(CONTROL_FLOW);
if (controlFlow == null) {
- myControlFlow = controlFlow = CachedValuesManager.getManager(getProject()).createCachedValue(new CachedValueProvider() {
+ controlFlow = CachedValuesManager.getManager(getProject()).createCachedValue(new CachedValueProvider() {
@Override
public Result compute() {
return Result.create(new ControlFlowBuilder(getProject()).buildControlFlow(GrBlockImpl.this), getContainingFile());
}
}, false);
+ putUserData(CONTROL_FLOW, controlFlow);
}
return controlFlow.getValue();
diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GrClassImplUtil.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GrClassImplUtil.java
index 74595e8790db..afbbc968ff33 100644
--- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GrClassImplUtil.java
+++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GrClassImplUtil.java
@@ -547,7 +547,8 @@ public class GrClassImplUtil {
builder.addModifier(PsiModifier.PUBLIC);
final PsiParameter[] originalParameters = method.getParameterList().getParameters();
for (PsiParameter originalParameter : originalParameters) {
- builder.addParameter(originalParameter.getName(), substitutor.substitute(originalParameter.getType()));
+ PsiType type = substitutor.substitute(originalParameter.getType());
+ builder.addParameter(originalParameter.getName(), type == null ? PsiType.getJavaLangObject(clazz.getManager(), clazz.getResolveScope()) : type);
}
builder.setBaseIcon(GroovyIcons.METHOD);
return builder;
diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/surroundWith/surrounders/surroundersImpl/expressions/GroovyWithTypeCastSurrounder.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/surroundWith/surrounders/surroundersImpl/expressions/GroovyWithTypeCastSurrounder.java
index 6724da5e03b6..a4f063e069e7 100644
--- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/surroundWith/surrounders/surroundersImpl/expressions/GroovyWithTypeCastSurrounder.java
+++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/surroundWith/surrounders/surroundersImpl/expressions/GroovyWithTypeCastSurrounder.java
@@ -29,13 +29,15 @@ import org.jetbrains.plugins.groovy.lang.psi.api.types.GrTypeElement;
public class GroovyWithTypeCastSurrounder extends GroovyExpressionSurrounder {
protected TextRange surroundExpression(GrExpression expression) {
GrParenthesizedExpression parenthesized = (GrParenthesizedExpression) GroovyPsiElementFactory.getInstance(expression.getProject()).createTopElementFromText("((Type)a)");
- parenthesized = (GrParenthesizedExpression) expression.replaceWithExpression(parenthesized, false);
GrTypeCastExpression typeCast = (GrTypeCastExpression) parenthesized.getOperand();
replaceToOldExpression(typeCast.getOperand(), expression);
GrTypeElement typeElement = typeCast.getCastTypeElement();
int endOffset = typeElement.getTextRange().getStartOffset();
+ parenthesized = (GrParenthesizedExpression) expression.replaceWithExpression(parenthesized, false);
- typeCast.getNode().removeChild(typeElement.getNode());
+ final GrTypeCastExpression newTypeCast = (GrTypeCastExpression)parenthesized.getOperand();
+ final GrTypeElement newTypeElement = newTypeCast.getCastTypeElement();
+ newTypeElement.delete();
return new TextRange(endOffset, endOffset);
}
diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/refactoring/introduce/constant/GrIntroduceConstantHandler.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/refactoring/introduce/constant/GrIntroduceConstantHandler.java
index 3e59ee304806..575381236aeb 100644
--- a/plugins/groovy/src/org/jetbrains/plugins/groovy/refactoring/introduce/constant/GrIntroduceConstantHandler.java
+++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/refactoring/introduce/constant/GrIntroduceConstantHandler.java
@@ -140,7 +140,14 @@ public class GrIntroduceConstantHandler extends GrIntroduceHandlerBase ref = new Ref(Boolean.TRUE);
+ final GrExpression finalExpression = expression;
expression.accept(new GroovyRecursiveElementVisitor() {
@Override
public void visitReferenceExpression(GrReferenceExpression refExpr) {
@@ -346,6 +351,10 @@ public class GrIntroduceFieldDialog extends DialogWrapper implements GrIntroduce
if (resolved instanceof GrField && scope.getManager().areElementsEquivalent(scope, ((GrField)resolved).getContainingClass())) {
return;
}
+ if (resolved instanceof PsiParameter &&
+ PsiTreeUtil.isAncestor(finalExpression, ((PsiParameter)resolved).getDeclarationScope(), false)) {
+ return;
+ }
ref.set(Boolean.FALSE);
}
});
diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/refactoring/introduce/field/GrIntroduceFieldHandler.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/refactoring/introduce/field/GrIntroduceFieldHandler.java
index 8e3df7a13309..e437d429fd19 100644
--- a/plugins/groovy/src/org/jetbrains/plugins/groovy/refactoring/introduce/field/GrIntroduceFieldHandler.java
+++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/refactoring/introduce/field/GrIntroduceFieldHandler.java
@@ -247,7 +247,13 @@ public class GrIntroduceFieldHandler extends GrIntroduceHandlerBasetestMethod()
+ names = testMethod()
names
}
diff --git a/plugins/groovy/testdata/groovy/refactoring/extractMethod/input1.test b/plugins/groovy/testdata/groovy/refactoring/extractMethod/input1.test
index b42d78fa4e79..a1cdec21b192 100644
--- a/plugins/groovy/testdata/groovy/refactoring/extractMethod/input1.test
+++ b/plugins/groovy/testdata/groovy/refactoring/extractMethod/input1.test
@@ -15,7 +15,7 @@ class S {
Closure sin = {x -> Math.sin(x)}
- testMethod(sin)
+ testMethod(sin)
}
diff --git a/plugins/groovy/testdata/refactoring/introduceParameterGroovy/closure/ClosureAfter.groovy b/plugins/groovy/testdata/refactoring/introduceParameterGroovy/closure/ClosureAfter.groovy
new file mode 100644
index 000000000000..5ec9c955f113
--- /dev/null
+++ b/plugins/groovy/testdata/refactoring/introduceParameterGroovy/closure/ClosureAfter.groovy
@@ -0,0 +1 @@
+new A().doSmth({ println "smth" })
\ No newline at end of file
diff --git a/plugins/groovy/testdata/refactoring/introduceParameterGroovy/closure/ClosureBefore.groovy b/plugins/groovy/testdata/refactoring/introduceParameterGroovy/closure/ClosureBefore.groovy
new file mode 100644
index 000000000000..df0b92127858
--- /dev/null
+++ b/plugins/groovy/testdata/refactoring/introduceParameterGroovy/closure/ClosureBefore.groovy
@@ -0,0 +1 @@
+new A().doSmth()
\ No newline at end of file
diff --git a/plugins/groovy/testdata/refactoring/introduceParameterGroovy/closure/ClosureMyClass.groovy b/plugins/groovy/testdata/refactoring/introduceParameterGroovy/closure/ClosureMyClass.groovy
new file mode 100644
index 000000000000..59a1a626d833
--- /dev/null
+++ b/plugins/groovy/testdata/refactoring/introduceParameterGroovy/closure/ClosureMyClass.groovy
@@ -0,0 +1,6 @@
+class A {
+ void doSmth() {
+ [1, 2, 3].each { println "smth" }
+ }
+}
+