Merge branch 'master' of git@git.labs.intellij.net:idea/community

This commit is contained in:
Kirill Kalishev
2011-03-11 16:24:11 +03:00
50 changed files with 1783 additions and 157 deletions
-6
View File
@@ -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()
@@ -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;
@@ -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) {
@@ -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;
@@ -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.
* <p/>
* 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<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 new String(data);
}
}
@@ -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;
@@ -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;
@@ -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;
}
}
@@ -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.
* <p/>
* 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.
* <p/>
* I.e. it's considered that given array contains particular text at <code>[0; length)</code> 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.
* <p/>
* Example: consider that initial text is <code>'12345'</code> and given changes are <code>'remove text at [1; 3) interval'</code>
* and <code>'replace text at [4; 5) interval with 'abcde''</code>. Resulting text is <code>'14abcde'</code> 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 <code>true</code> if the first change in a group is found; <code>false</code> 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();
}
}
}
@@ -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 <code>'defer changes'</code> 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<String> 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<String>(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 <code>true</code> if this object is at {@link #setDeferredChangeMode(boolean) defer changes} mode;
* <code>false</code> 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.
* <p/>
* This method allows to define that <code>'defer changes'</code> mode usages, i.e. expected usage pattern is as follows:
* <pre>
* <ol>
* <li>
* Client of this class enters <code>'defer changes'</code> mode (calls this method with <code>'true'</code> argument).
* That means that all subsequent changes will not actually modify backed array data and will be stored separately;
* </li>
* <li>
* 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});
* </li>
* <li>
* Client of this class indicates that <code>'massive change time'</code> is over by calling this method with <code>'false'</code>
* 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;
* </li>
* </ol>
* </pre>
* <p/>
* <b>Note:</b> we can't exclude possibility that <code>'defer changes'</code> mode is started but inadvertently not ended
* (due to programming error, unexpected exception etc). Hence, this class is free to automatically end
* <code>'defer changes'</code> mode when necessary in order to avoid memory leak with infinite deferred changes storing.
*
* @param deferredChangeMode flag that defines if <code>'defer changes'</code> mode should be used by the current object
*/
public void setDeferredChangeMode(boolean deferredChangeMode) {
myDeferredChangeMode = deferredChangeMode;
if (!deferredChangeMode) {
flushDeferredChanged();
}
}
private void flushDeferredChanged() {
List<TextChangeImpl> 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();
}
}
@@ -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();
}
@@ -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;
@@ -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.
* <p/>
* Provides ability to automatic merging them if necessary.
* <p/>
* Not thread-safe.
*
* @author Denis Zhdanov
* @since 3/2/11 11:55 AM
*/
public class TextChangesStorage {
private final List<ChangeEntry> myChanges = new ArrayList<ChangeEntry>();
/**
* @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<TextChangeImpl> getChanges() {
List<TextChangeImpl> result = new ArrayList<TextChangeImpl>();
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<TextChange> result = null;
for (int i = changeStartIndex; i <= changeEndIndex; i++) {
if (!endInclusive && i == changeEndIndex) {
break;
}
if (result == null) {
result = new ArrayList<TextChange>();
}
result.add(myChanges.get(i).change);
}
return result == null ? Collections.<TextChange>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.
* <p/>
* <b>Note:</b> it's assumed that given change offsets are related to the current state of the text (<code>'client text'</code>),
* i.e. with all stored changes applied to it. Example:
* <ol>
* <li>Say, we have initial text <code>'12345'</code>;</li>
* <li>
* Suppose the change <code>'replace text at [2; 3) range with 'ABC''</code> is applied to it (stored at the current object).
* End-users see the text <code>'12ABC45'</code> now;
* </li>
* <li>
* This method is called with change like <code>'replace text at [1; 6) range with 'XY''</code>. Change range is assumed to
* be related to the text visible to end-user, not initial one (<code>'12ABC45'</code>, not <code>'12345'</code>).
* I.e. the user will see text <code>'1XY5'</code> now;
* </li>
* </ol>
*
* @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.
* <p/>
* Example:
* <pre>
* <ul>
* <li>Consider that original text is <code>'01234'</code>;</li>
* <li>
* Consider that two changes are registered: <code>'insert text 'a' at index 1'</code> and
* <code>'insert text 'bc' at index 3'</code>;
* </li>
* <li><code>'client text'</code> now is <code>'0a12bc34'</code>;</li>
* <li>This method is called with index '5' - symbol 'c' is returned;</li>
* </ul>
* </pre>
*
* @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 <code>'client text'</code>)
* or index of the first change that lays after the given offset.
*
* @param clientOffset target offset against the <code>'client text'</code>
* @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 <code>'-returned_index - 1'</code> 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;
}
}
}
@@ -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;
/**
@@ -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;
@@ -104,7 +104,7 @@ public abstract class SpeedSearchBase<Comp extends JComponent> 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<Comp extends JComponent> 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<Comp extends JComponent> 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);
}
@@ -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<Pair<Integer, Integer>>());
}
private void checkSubSequence(@NotNull String expected, @NotNull CharSequence actual,
@NotNull Stack<Pair<Integer, Integer>> 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<Integer, Integer>(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;
}
}
@@ -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));
}
}
@@ -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;
@@ -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;
@@ -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
@@ -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 {
@@ -488,7 +488,7 @@ public class ContainerUtil {
}
}
public static <T> Collection<T> addAll(@NotNull Collection<T> collection, @NotNull T... elements) {
public static <T, A extends T, C extends Collection<T>> 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 <T> Set<T> set(T ... items) {
return addAll(new HashSet<T>(), items);
}
public static <T> void addIfNotNull(final T element, @NotNull Collection<T> result) {
if (element != null) {
result.add(element);
@@ -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);
}
}
@@ -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.
* <p/>
* 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.
* <p/>
* 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 <code>'values'</code> collection.
*/
private static final Map<String, Set<String>> REPLACEABLE_VALIDATOR_TYPES = new HashMap<String, Set<String>>();
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<Validator> parameters = new ArrayList<Validator>();
@@ -108,11 +133,13 @@ class FormatDecode{
int argumentCount){
if(pos < parameters.size()){
final Validator old = parameters.get(pos);
Set<String> 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());
}
@@ -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() {
@@ -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);
}
}
@@ -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();
@@ -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);
}
}
/**
@@ -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) {
@@ -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<Instruction[]> myControlFlow = null;
private static final Key<CachedValue<Instruction[]>> 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<Instruction[]> controlFlow = myControlFlow;
CachedValue<Instruction[]> controlFlow = getUserData(CONTROL_FLOW);
if (controlFlow == null) {
myControlFlow = controlFlow = CachedValuesManager.getManager(getProject()).createCachedValue(new CachedValueProvider<Instruction[]>() {
controlFlow = CachedValuesManager.getManager(getProject()).createCachedValue(new CachedValueProvider<Instruction[]>() {
@Override
public Result<Instruction[]> compute() {
return Result.create(new ControlFlowBuilder(getProject()).buildControlFlow(GrBlockImpl.this), getContainingFile());
}
}, false);
putUserData(CONTROL_FLOW, controlFlow);
}
return controlFlow.getValue();
@@ -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;
@@ -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);
}
@@ -140,7 +140,14 @@ public class GrIntroduceConstantHandler extends GrIntroduceHandlerBase<GrIntrodu
}
private static void replaceOccurence(GrField field, PsiElement occurence, boolean escalateVisibility) {
final PsiElement replaced = occurence.replace(createRefExpression(field, occurence));
final PsiElement replaced;
final GrReferenceExpression newExpr = createRefExpression(field, occurence);
if (occurence instanceof GrExpression) {
replaced = ((GrExpression)occurence).replaceWithExpression(newExpr, false);
}
else {
replaced = occurence.replace(newExpr);
}
if (escalateVisibility) {
PsiUtil.escalateVisibility(field, replaced);
}
@@ -20,8 +20,11 @@ import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.introduceField.IntroduceFieldHandler;
import com.intellij.refactoring.ui.NameSuggestionsField;
import com.intellij.refactoring.util.RadioUpDownListener;
import com.intellij.util.IncorrectOperationException;
@@ -165,6 +168,7 @@ public class GrIntroduceFieldDialog extends DialogWrapper implements GrIntroduce
allOccurrencesInOneMethod(myContext.occurrences, clazz) && isAlwaysInvokedConstructor(containingMethod, clazz);
hasLHSUsages = hasLhsUsages(myContext);
setTitle(IntroduceFieldHandler.REFACTORING_NAME);
init();
checkErrors();
}
@@ -337,6 +341,7 @@ public class GrIntroduceFieldDialog extends DialogWrapper implements GrIntroduce
}
final Ref<Boolean> ref = new Ref<Boolean>(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);
}
});
@@ -247,7 +247,13 @@ public class GrIntroduceFieldHandler extends GrIntroduceHandlerBase<GrIntroduceF
}
private static void replaceOccurence(GrField field, PsiElement occurence) {
final PsiElement replaced = occurence.replace(createRefExpression(field, occurence));
final GrReferenceExpression newExpr = createRefExpression(field, occurence);
final PsiElement replaced;
if (occurence instanceof GrExpression) {
replaced = ((GrExpression)occurence).replaceWithExpression(newExpr, false);
} else {
replaced = occurence.replace(newExpr);
}
if (replaced instanceof GrQualifiedReference) {
if (!PsiUtil.shortenReference((GrQualifiedReference)replaced)) {
final PsiElement qualifier = ((GrQualifiedReference)replaced).getQualifier();
@@ -19,6 +19,7 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiType;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.refactoring.JavaRefactoringSettings;
import com.intellij.refactoring.inline.InlineParameterHandler;
import com.intellij.refactoring.ui.NameSuggestionsField;
import com.intellij.refactoring.ui.RefactoringDialog;
import com.intellij.util.ui.GridBag;
@@ -84,6 +85,8 @@ public class GrIntroduceParameterDialog extends RefactoringDialog implements GrI
initReplaceFieldsWithGetters(settings);
myDeclareFinalCheckBox.setSelected(hasFinalModifier());
setTitle(InlineParameterHandler.REFACTORING_NAME);
init();
}
@@ -310,12 +310,14 @@ public class GrIntroduceParameterProcessor extends BaseRefactoringProcessor impl
}
else if (usage instanceof InternalUsageInfo) {
PsiElement element = usage.getElement();
if (element instanceof GrExpression) {
element = PsiUtil.skipParentheses(element, true);
}
if (element == null) continue;
GrExpression newExpr = factory.createExpressionFromText(mySettings.getName());
element.replace(newExpr);
if (element instanceof GrExpression) {
((GrExpression)element).replaceWithExpression(newExpr, true);
}
else {
element.replace(newExpr);
}
}
}
@@ -289,4 +289,8 @@ public class GrIntroduceParameterTest extends LightCodeInsightFixtureTestCase {
public void testIncorrectArgumentList() {
doTest(IntroduceParameterRefactoring.REPLACE_FIELDS_WITH_GETTERS_NONE, true, false, true);
}
public void testClosure() {
doTest(IntroduceParameterRefactoring.REPLACE_FIELDS_WITH_GETTERS_NONE, false, false, false);
}
}
@@ -6,7 +6,7 @@ protected def getGeneratedFileNames(String name, int boo) {
-----
protected def getGeneratedFileNames(String name, int boo) {
def names
names = <caret>testMethod()
names = <caret>testMethod()
names
}
@@ -15,7 +15,7 @@ class S {
Closure sin = {x -> Math.sin(x)}
<caret>testMethod(sin)
<caret>testMethod(sin)
}
@@ -0,0 +1 @@
new A().doSmth({ println "smth" })
@@ -0,0 +1 @@
new A().doSmth()
@@ -0,0 +1,6 @@
class A {
void doSmth() {
[1, 2, 3].each <selection>{ println "smth" }</selection>
}
}