| /* |
| * Copyright 2000-2014 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.diagnostic.Dumpable; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.impl.ApplicationInfoImpl; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.event.DocumentEvent; |
| import com.intellij.openapi.util.text.StringUtil; |
| 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.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.lang.ref.Reference; |
| import java.lang.ref.SoftReference; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReentrantLock; |
| |
| /** |
| * @author cdr |
| */ |
| abstract class CharArray implements CharSequenceBackedByArray, Dumpable { |
| private static final boolean CHECK_DOCUMENT_CONSISTENCY = ApplicationManager.getApplication() != null && ApplicationManager.getApplication().isUnitTestMode(); |
| private static final Logger LOG = Logger.getInstance("#" + CharArray.class.getName()); |
| |
| @SuppressWarnings("UseOfArchaicSystemPropertyAccessors") |
| private static final boolean DISABLE_DEFERRED_PROCESSING = Boolean.getBoolean("idea.document.deny.deferred.changes"); |
| |
| @SuppressWarnings("UseOfArchaicSystemPropertyAccessors") |
| private static final boolean DEBUG_DEFERRED_PROCESSING = LOG.isDebugEnabled() || Boolean.getBoolean("idea.document.debug.bulk.processing"); |
| /** |
| * 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; |
| |
| private final TextChangesStorage myDeferredChangesStorage; |
| |
| private volatile int myStart; // start offset in myArray (used as an optimization when call substring()) |
| private volatile int myCount; |
| |
| private volatile CharSequence myOriginalSequence; |
| private volatile char[] myArray; |
| private volatile Reference<String> myStringRef; // buffers String value - for not to generate it every time |
| private volatile int myBufferSize; |
| private volatile int myDeferredShift; |
| private volatile boolean myDeferredChangeMode; |
| private volatile boolean myHasDeferredChanges; |
| // this lock is for mutual exclusion during read action access |
| // (some fields are changed in read action too) |
| private final Lock lock = new ReentrantLock(); |
| |
| // We had a problems with bulk document text processing, hence, debug facilities were introduced. The fields group below work with them. |
| // The main idea is to hold all history of bulk processing iteration in order to be able to retrieve it from client and reproduce the |
| // problem. |
| |
| private final boolean myDebug = isDebug(); |
| |
| boolean isDebug() { |
| return DEBUG_DEFERRED_PROCESSING || CHECK_DOCUMENT_CONSISTENCY && !ApplicationInfoImpl.isInPerformanceTest(); |
| } |
| |
| /** |
| * Duplicate instance of the current char array that is used during debug processing as follows - apply every text change |
| * from the bulk changes group to this instance immediately in order to be able to check if the current 'deferred change-aware' |
| * instance functionally behaves at the same way as 'straightforward' one. |
| */ |
| private CharArray myDebugArray; |
| |
| /** |
| * Holds deferred changes create during the current bulk processing iteration. |
| */ |
| private List<TextChangeImpl> myDebugDeferredChanges; |
| |
| /** |
| * Document text on bulk processing start. |
| */ |
| private String myDebugTextOnBatchUpdateStart; |
| |
| // bufferSize == 0 means unbounded |
| CharArray(final int bufferSize, @NotNull char[] data, int length) { |
| myBufferSize = bufferSize; |
| myDeferredChangesStorage = new TextChangesStorage(); |
| myArray = Arrays.copyOf(data, length); |
| myCount = length; |
| |
| if (myDebug) { |
| myDebugArray = new CharArray(bufferSize, data, length) { |
| @NotNull |
| @Override |
| protected DocumentEvent beforeChangedUpdate(int offset, |
| CharSequence oldString, |
| CharSequence newString, |
| boolean wholeTextReplaced) { |
| return CharArray.this.beforeChangedUpdate(offset, oldString, newString, wholeTextReplaced); |
| } |
| |
| @Override |
| protected void afterChangedUpdate(@NotNull DocumentEvent event, long newModificationStamp) { |
| } |
| |
| @Override |
| protected void assertWriteAccess() { |
| } |
| |
| @Override |
| protected void assertReadAccess() { |
| } |
| |
| @Override |
| boolean isDebug() { |
| return false; |
| } |
| }; |
| myDebugDeferredChanges = new ArrayList<TextChangeImpl>(); |
| } |
| assertConsistency(); |
| } |
| |
| public void setBufferSize(int bufferSize) { |
| assert bufferSize >= 0 : bufferSize; |
| myBufferSize = bufferSize; |
| assertConsistency(); |
| } |
| |
| private DocumentEvent startChange(int offset, |
| @Nullable CharSequence oldString, |
| @Nullable CharSequence newString, |
| boolean wholeTextReplaced) { |
| assert myStart == 0; // can't change substring |
| assertWriteAccess(); |
| assertConsistency(); |
| |
| return beforeChangedUpdate(offset, oldString, newString, wholeTextReplaced); |
| } |
| |
| @NotNull |
| protected abstract DocumentEvent beforeChangedUpdate(int offset, |
| @Nullable CharSequence oldString, |
| @Nullable CharSequence newString, |
| boolean wholeTextReplaced); |
| protected abstract void afterChangedUpdate(@NotNull DocumentEvent event, long newModificationStamp); |
| |
| protected abstract void assertWriteAccess(); |
| protected abstract void assertReadAccess(); |
| |
| private void setText(@NotNull CharSequence chars) { |
| assertConsistency(); |
| myOriginalSequence = chars.toString(); |
| myArray = null; |
| myStringRef = null; |
| myCount = chars.length(); |
| assert myStart == 0; // can't change substring |
| myDeferredChangesStorage.clear(); |
| myHasDeferredChanges = false; |
| trimToSize(); |
| |
| if (myDebug) { |
| myDebugArray.setText(chars); |
| myDebugDeferredChanges.clear(); |
| } |
| assertConsistency(); |
| } |
| |
| private void assertConsistency() { |
| if (isDeferredChangeMode()) { |
| assert myOriginalSequence == null; |
| } |
| CharSequence originalSequence = myOriginalSequence; |
| int origLen = originalSequence == null ? -1 : originalSequence.length(); |
| String string = com.intellij.reference.SoftReference.dereference(myStringRef); |
| int stringLen = string == null ? -1 : string.length(); |
| assert origLen == stringLen || origLen==-1 || stringLen==-1; |
| |
| int count = myCount + myDeferredShift; |
| assert count == origLen || origLen==-1; |
| assert count == stringLen || stringLen==-1; |
| |
| if (!myDebug) return; |
| final CharSequence seqFromCharArray; |
| |
| if (myArray != null) { |
| assert myCount <= myArray.length; |
| seqFromCharArray = new CharArrayCharSequence(myArray, myStart, myCount); |
| } |
| else { |
| seqFromCharArray = null; |
| } |
| |
| if (seqFromCharArray != null && originalSequence != null) { |
| assert StringUtil.equals(seqFromCharArray, originalSequence); |
| } |
| if (!isDeferredChangeMode() && seqFromCharArray != null && string != null) { |
| assert StringUtil.equals(seqFromCharArray, string); |
| } |
| if (originalSequence != null && string != null) { |
| assert string.equals(originalSequence.toString()); |
| } |
| |
| myDebugArray.assertConsistency(); |
| |
| CharSequence str = com.intellij.reference.SoftReference.dereference(myStringRef); |
| if (str == null) { |
| if (myHasDeferredChanges) { |
| str = doSubString(0, myCount + myDeferredShift).toString(); |
| } |
| else if (myOriginalSequence != null) { |
| str = myOriginalSequence.toString(); |
| } |
| else { |
| str = seqFromCharArray; |
| } |
| } |
| assert count == str.length(); |
| if (isDeferredChangeMode()) { |
| String expected = myDebugArray.toString(); |
| checkStrings("toString()", expected, str); |
| } |
| } |
| |
| public void replace(int startOffset, |
| int endOffset, |
| @NotNull CharSequence toDelete, |
| @NotNull CharSequence newString, |
| long newModificationStamp, |
| boolean wholeTextReplaced) { |
| final DocumentEvent event = startChange(startOffset, toDelete, newString, wholeTextReplaced); |
| |
| startOffset += myStart; |
| endOffset += myStart; |
| doReplace(startOffset, endOffset, newString); |
| afterChangedUpdate(event, newModificationStamp); |
| assertConsistency(); |
| } |
| |
| private void doReplace(int startOffset, int endOffset, @NotNull CharSequence newString) { |
| prepareForModification(); |
| |
| if (isDeferredChangeMode()) { |
| storeChange(new TextChangeImpl(newString, startOffset, endOffset)); |
| if (myDebug) { |
| myDebugArray.doReplace(startOffset, endOffset, newString); |
| } |
| } |
| else { |
| int newLength = newString.length(); |
| int oldLength = endOffset - startOffset; |
| |
| CharArrayUtil.getChars(newString, myArray, startOffset, Math.min(newLength, oldLength)); |
| myStringRef = null; |
| |
| if (newLength > oldLength) { |
| doInsert(newString.subSequence(oldLength, newLength), endOffset); |
| } |
| else if (newLength < oldLength) { |
| doRemove(startOffset + newLength, startOffset + oldLength); |
| } |
| } |
| } |
| |
| public void remove(int startIndex, int endIndex, @NotNull CharSequence toDelete) { |
| DocumentEvent event = startChange(startIndex, toDelete, null, false); |
| startIndex += myStart; |
| endIndex += myStart; |
| doRemove(startIndex, endIndex); |
| afterChangedUpdate(event, LocalTimeCounter.currentTime()); |
| assertConsistency(); |
| } |
| |
| private void doRemove(int startIndex, int endIndex) { |
| if (startIndex == endIndex) { |
| return; |
| } |
| prepareForModification(); |
| |
| if (isDeferredChangeMode()) { |
| storeChange(new TextChangeImpl("", startIndex, endIndex)); |
| if (myDebug) { |
| myDebugArray.doRemove(startIndex, endIndex); |
| } |
| } |
| else { |
| if (endIndex < myCount) { |
| System.arraycopy(myArray, endIndex, myArray, startIndex, myCount - endIndex); |
| myStringRef = null; |
| } |
| myCount -= endIndex - startIndex; |
| } |
| } |
| |
| public void insert(@NotNull CharSequence s, int startIndex) { |
| DocumentEvent event = startChange(startIndex, null, s, false); |
| startIndex += myStart; |
| doInsert(s, startIndex); |
| |
| afterChangedUpdate(event, LocalTimeCounter.currentTime()); |
| trimToSize(); |
| assertConsistency(); |
| } |
| |
| private void doInsert(@NotNull CharSequence s, final int startIndex) { |
| prepareForModification(); |
| |
| if (isDeferredChangeMode()) { |
| storeChange(new TextChangeImpl(s, startIndex)); |
| if (myDebug) { |
| myDebugArray.doInsert(s, startIndex); |
| } |
| } |
| else { |
| int insertLength = s.length(); |
| myArray = resizeArray(myArray, myCount + insertLength); |
| if (startIndex < myCount) { |
| System.arraycopy(myArray, startIndex, myArray, startIndex + insertLength, myCount - startIndex); |
| } |
| |
| CharArrayUtil.getChars(s, myArray, startIndex); |
| myCount += insertLength; |
| myStringRef = null; |
| } |
| } |
| |
| /** |
| * 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 (!change.isWithinBounds(length())) { |
| LOG.error( |
| "Invalid change attempt detected - given change bounds are not within the current char array. Change: " + |
| change.getText().length()+":" + change.getStart()+"-" + change.getEnd(), dumpState()); |
| return; |
| } |
| if (myDeferredChangesStorage.size() >= MAX_DEFERRED_CHANGES_NUMBER) { |
| flushDeferredChanged(); |
| } |
| myDeferredChangesStorage.store(change); |
| myHasDeferredChanges = true; |
| myDeferredShift += change.getDiff(); |
| |
| if (myDebug) { |
| myDebugDeferredChanges.add(change); |
| } |
| } |
| |
| private void prepareForModification() { |
| if (myOriginalSequence != null) { |
| myArray = new char[myOriginalSequence.length()]; |
| CharArrayUtil.getChars(myOriginalSequence, myArray, 0); |
| myCount = myArray.length; |
| myOriginalSequence = null; |
| myStart = 0; |
| } |
| myStringRef = null; |
| |
| assertConsistency(); |
| } |
| |
| @NotNull |
| public CharSequence getCharArray() { |
| assertConsistency(); |
| CharSequence originalSequence = myOriginalSequence; |
| return originalSequence == null ? this : originalSequence; |
| } |
| |
| @NotNull |
| public String toString() { |
| assertConsistency(); |
| String str = com.intellij.reference.SoftReference.dereference(myStringRef); |
| if (str == null) { |
| if (myHasDeferredChanges) { |
| str = substring(0, length()).toString(); |
| } |
| else { |
| str = myOriginalSequence == null ? new String(myArray, myStart, myCount) : myOriginalSequence.toString(); |
| } |
| myStringRef = new SoftReference<String>(str); |
| } |
| return str; |
| } |
| |
| @Override |
| public final int length() { |
| final int result = myCount + myDeferredShift; |
| if (myDebug && isDeferredChangeMode()) { |
| int expected = myDebugArray.length(); |
| if (expected != result) { |
| dumpDebugInfo("Incorrect length() processing. Expected: '" + expected + "', actual: '" + result + "'"); |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| public final char charAt(int i) { |
| if (i < 0 || i >= length()) { |
| throw new IndexOutOfBoundsException("Wrong offset: " + i + "; count:" + length()); |
| } |
| i += myStart; |
| final char result; |
| if (!myHasDeferredChanges) { |
| if (myOriginalSequence != null) { |
| result = myOriginalSequence.charAt(i); |
| } |
| else { |
| result = myArray[i]; |
| } |
| } |
| else { |
| result = myDeferredChangesStorage.charAt(myArray, i); |
| } |
| |
| if (myDebug && isDeferredChangeMode()) { |
| char expected = myDebugArray.charAt(i); |
| if (expected != result) { |
| dumpDebugInfo("Incorrect charAt() processing for index " + i + ". Expected: '" + expected + "', actual: '" + result + "'"); |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| @NotNull |
| public CharSequence subSequence(final int start, final int end) { |
| assertReadAccess(); |
| assertConsistency(); |
| if (start == 0 && end == length()) return this; |
| if (myOriginalSequence != null) { |
| return myOriginalSequence.subSequence(start, end); |
| } |
| flushDeferredChanged(); |
| return new CharArrayCharSequence(myArray, start, end); |
| } |
| |
| @Override |
| @NotNull |
| public char[] getChars() { |
| assertReadAccess(); |
| assertConsistency(); |
| char[] array = myArray; |
| CharSequence originalSequence = myOriginalSequence; |
| if (myHasDeferredChanges || originalSequence != null && array == null) { |
| // slow track |
| lock.lock(); |
| try { |
| flushDeferredChanged(); |
| if (myOriginalSequence != null && myArray == null) { |
| myArray = array = CharArrayUtil.fromSequence(myOriginalSequence); |
| myStringRef = null; |
| } |
| } |
| finally { |
| lock.unlock(); |
| } |
| assertConsistency(); |
| } |
| return array; |
| } |
| |
| @Override |
| public void getChars(@NotNull final char[] dst, final int dstOffset) { |
| assertReadAccess(); |
| assertConsistency(); |
| flushDeferredChanged(); |
| if (myOriginalSequence == null) { |
| System.arraycopy(myArray, myStart, dst, dstOffset, length()); |
| } |
| else { |
| CharArrayUtil.getChars(myOriginalSequence, dst, dstOffset); |
| } |
| |
| if (myDebug && isDeferredChangeMode()) { |
| char[] expected = new char[dst.length]; |
| myDebugArray.getChars(expected, dstOffset); |
| for (int i = dstOffset, j = myStart; i < dst.length && j < myArray.length; i++, j++) { |
| if (expected[i] != myArray[j]) { |
| dumpDebugInfo("getChars(char[], int). Given array of length " + dst.length + ", offset " + dstOffset + ". Found char '" + myArray[j] + |
| "' at index " + i + ", expected to find '" + expected[i] + "'"); |
| break; |
| } |
| } |
| } |
| } |
| |
| @NotNull |
| public CharSequence substring(final int start, final int end) { |
| assertReadAccess(); |
| final CharSequence result = doSubString(start, end); |
| |
| assertConsistency(); |
| return result; |
| } |
| |
| private CharSequence doSubString(int start, int end) { |
| if (start == end) return ""; |
| final CharSequence result; |
| if (myOriginalSequence == null) { |
| result = myDeferredChangesStorage.substring(myArray, start + myStart, end + myStart); |
| } |
| else { |
| result = myOriginalSequence.subSequence(start, end); |
| } |
| return result; |
| } |
| |
| @NotNull |
| private static char[] resizeArray(@NotNull char[] array, int newSize) { |
| if (newSize < array.length) { |
| return array; |
| } |
| |
| int newArraySize = array.length; |
| if (newArraySize == 0) { |
| newArraySize = 16; |
| } |
| while (newArraySize <= newSize) { |
| newArraySize = newArraySize * 12 / 10 + 1; |
| } |
| char[] newArray = new char[newArraySize]; |
| System.arraycopy(array, 0, newArray, 0, array.length); |
| return newArray; |
| } |
| |
| private void trimToSize() { |
| if (myBufferSize != 0 && length() > myBufferSize) { |
| flushDeferredChanged(); |
| |
| // make a copy |
| int endIndex = myCount - myBufferSize; |
| String toDelete = getCharArray().subSequence(0, endIndex).toString(); |
| remove(0, endIndex, toDelete); |
| } |
| } |
| |
| /** |
| * @return <code>true</code> if this object is in the defer changes mode, see {@link #setDeferredChangeMode(boolean)}; |
| */ |
| public boolean isDeferredChangeMode() { |
| return myDeferredChangeMode; |
| } |
| |
| public boolean hasDeferredChanges() { |
| return myHasDeferredChanges; |
| } |
| |
| /** |
| * 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(CharSequence, int) insert}, |
| * {@link #remove(int, int, CharSequence) remove} and |
| * {@link #replace(int, int, java.lang.CharSequence, java.lang.CharSequence, long, boolean)}); |
| * </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) { |
| if (!DISABLE_DEFERRED_PROCESSING) { |
| if (deferredChangeMode) { |
| if (myDebug) { |
| myDebugArray.setText(myDebugTextOnBatchUpdateStart = toString()); |
| myDebugDeferredChanges.clear(); |
| } |
| prepareForModification(); |
| myDeferredChangeMode = deferredChangeMode; |
| } |
| else { |
| myDeferredChangeMode = deferredChangeMode; |
| flushDeferredChanged(); |
| } |
| } |
| assertConsistency(); |
| } |
| |
| private void flushDeferredChanged() { |
| List<TextChangeImpl> changes = myDeferredChangesStorage.getChanges(); |
| if (changes.isEmpty()) { |
| return; |
| } |
| |
| lock.lock(); |
| try { |
| char[] beforeMerge = null; |
| if (myDebug) { |
| beforeMerge = new char[myArray.length]; |
| System.arraycopy(myArray, 0, beforeMerge, 0, myArray.length); |
| } |
| |
| BulkChangesMerger changesMerger = BulkChangesMerger.INSTANCE; |
| final boolean inPlace; |
| if (myArray.length < length()) { |
| myArray = changesMerger.mergeToCharArray(myArray, myCount, changes); |
| inPlace = false; |
| } |
| else { |
| changesMerger.mergeInPlace(myArray, myCount, changes); |
| inPlace = true; |
| } |
| |
| myCount += myDeferredShift; |
| myDeferredShift = 0; |
| myDeferredChangesStorage.clear(); |
| myHasDeferredChanges = false; |
| myDeferredChangeMode = false; |
| myStringRef = null; |
| |
| if (myDebug) { |
| for (int i = 0, max = length(); i < max; i++) { |
| if (myArray[i] != myDebugArray.myArray[i]) { |
| dumpDebugInfo("flushDeferredChanged(). Index " + i + ", expected: '" + myDebugArray.myArray[i]+"', actual '" + |
| myArray[i]+"'. Text before merge: '" + Arrays.toString(beforeMerge)+"', merge inplace: "+inPlace); |
| break; |
| } |
| } |
| } |
| } |
| finally { |
| lock.unlock(); |
| } |
| assertConsistency(); |
| } |
| |
| @Override |
| @NonNls |
| @NotNull |
| public String dumpState() { |
| return "deferred changes mode: " + isDeferredChangeMode()+", length: " + length()+" (data array length: " + myCount+ |
| ", deferred shift: " + myDeferredShift+"); view offsets: [" + myStart+"; "+myCount+"]; deferred changes: "+myDeferredChangesStorage; |
| } |
| |
| private void checkStrings(@NonNls @NotNull String operation, @NotNull String expected, @NotNull CharSequence actual) { |
| if (StringUtil.equals(expected, actual)) { |
| return; |
| } |
| for (int i = 0, max = Math.min(expected.length(), actual.length()); i < max; i++) { |
| if (actual.charAt(i) != expected.charAt(i)) { |
| dumpDebugInfo( |
| "Incorrect " + |
| operation+" processing. Expected length: " + |
| expected.length()+", actual length: " + |
| actual.length()+". Unmatched symbol at " + |
| i+" - expected: '" + |
| expected.charAt(i)+"', " + |
| "actual: '" + |
| actual.charAt(i)+"', expected document: '" + |
| expected+"', actual document: '" + |
| actual+"'" |
| ); |
| return; |
| } |
| } |
| dumpDebugInfo("Incorrect " + operation+" processing. Expected length: " + expected.length()+", actual length: " + |
| actual.length()+", expected: '" + expected+"', actual: '" + actual+"'"); |
| } |
| |
| private void dumpDebugInfo(@NonNls @NotNull String problem) { |
| LOG.error( |
| "Incorrect CharArray processing detected: " + problem + |
| ". Start: " + myStart |
| + ", count: " + myCount + ", text on batch update start: " + |
| myDebugTextOnBatchUpdateStart + ", deferred changes history: " + |
| myDebugDeferredChanges + ", current deferred changes: " + myDeferredChangesStorage |
| ); |
| } |
| } |