mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Merge branch 'master' of git@git.labs.intellij.net:idea/community
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+3
-3
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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();
|
||||
}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
+1
@@ -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;
|
||||
}
|
||||
}
|
||||
+277
@@ -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));
|
||||
}
|
||||
}
|
||||
+1
@@ -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;
|
||||
|
||||
|
||||
+1
@@ -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());
|
||||
}
|
||||
|
||||
+1
@@ -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() {
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+5
@@ -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) {
|
||||
|
||||
+6
-4
@@ -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;
|
||||
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -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);
|
||||
}
|
||||
|
||||
+9
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
+7
-1
@@ -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();
|
||||
|
||||
+3
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
new A().doSmth({ println "smth" })
|
||||
+1
@@ -0,0 +1 @@
|
||||
new A().doSmth()
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
class A {
|
||||
void doSmth() {
|
||||
[1, 2, 3].each <selection>{ println "smth" }</selection>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user