blob: 96eb9afcab7eaf4cf86e23d396ca49c42883d4c2 [file] [log] [blame]
/*
* 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.diagnostic.Logger;
import com.intellij.openapi.editor.TextChange;
import com.intellij.util.text.CharArrayCharSequence;
import com.intellij.util.text.StringFactory;
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();
private static final Logger LOG = Logger.getInstance("#" + BulkChangesMerger.class.getName());
/**
* 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 StringFactory.createShared(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 increases 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
// less or equal 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
));
}
try {
for (Context context = new Context(changes, data, length, length + diff); !context.isComplete();) {
if (!context.startGroup()) {
return;
}
context.endGroup();
}
}
catch (RuntimeException e) {
StringBuilder changesDescription = new StringBuilder();
for (TextChangeImpl change : changes) {
changesDescription.append(change.getText().length()).append(":").append(change.getStart()).append("-").append(change.getEnd())
.append(",");
}
if (changesDescription.length() > 0) {
changesDescription.setLength(changesDescription.length() - 1);
}
LOG.error(String.format(
"Invalid attempt to perform in-place document changes merge detected. Initial text length: %d, data array length: %d, "
+ "changes: [%s], changes diff: %d", length, data.length, changesDescription, diff
), e);
char[] merged = mergeToCharArray(data, length, changes);
System.arraycopy(merged, 0, data, 0, length + diff);
}
}
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();
}
}
}