| /* |
| * 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 com.intellij.util.text.StringFactory; |
| import org.jetbrains.annotations.NonNls; |
| 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() { |
| if (myChanges.isEmpty()) return Collections.emptyList(); |
| List<TextChangeImpl> result = new ArrayList<TextChangeImpl>(myChanges.size()); |
| |
| 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 newChangeStart = change.getStart(); |
| int newChangeEnd = change.getEnd(); |
| int insertionIndex = getChangeIndex(change.getStart()); |
| int clientShift = 0; // 'Client text' shift before the given change to store. I.e. this value can be subtracted from the |
| // given change's start/end offsets in order to get original document range affected by the given change. |
| int changeDiff = change.getText().length() - (change.getEnd() - change.getStart()); |
| |
| if (insertionIndex < 0) { |
| insertionIndex = -insertionIndex - 1; |
| if (insertionIndex >= myChanges.size()) { |
| if (!myChanges.isEmpty()) { |
| ChangeEntry changeEntry = myChanges.get(myChanges.size() - 1); |
| clientShift = changeEntry.clientStartOffset - changeEntry.change.getStart() + changeEntry.change.getDiff(); |
| } |
| myChanges.add(new ChangeEntry( |
| new TextChangeImpl(change.getText(), change.getStart() - clientShift, change.getEnd() - clientShift), |
| change.getStart() |
| )); |
| return insertionIndex; |
| } |
| else if (insertionIndex > 0 && !myChanges.isEmpty()) { |
| ChangeEntry changeEntry = myChanges.get(insertionIndex - 1); |
| clientShift = changeEntry.clientStartOffset - changeEntry.change.getStart() + changeEntry.change.getDiff(); |
| } |
| } |
| else { |
| ChangeEntry changeEntry = myChanges.get(insertionIndex); |
| clientShift = changeEntry.clientStartOffset - changeEntry.change.getStart(); |
| } |
| |
| boolean updateClientOffsetOnly = false; |
| for (int i = insertionIndex; 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 after the new one. |
| if (!updateClientOffsetOnly && storedClientStart > newChangeEnd) { |
| if (changeDiff != 0) { |
| updateClientOffsetOnly = true; |
| } |
| else { |
| break; |
| } |
| } |
| |
| if (updateClientOffsetOnly) { |
| changeEntry.clientStartOffset += changeDiff; |
| continue; |
| } |
| |
| // Stored change lays before the new one. |
| if (storedClientEnd <= newChangeStart) { |
| clientShift += changeEntry.change.getDiff(); |
| insertionIndex = i + 1; |
| continue; |
| } |
| |
| // Check if given change targets 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; |
| updateClientOffsetOnly = true; |
| i--; // Assuming that 'i' is incremented at the 'for' loop. |
| continue; |
| } |
| |
| changeEntry.change = new TextChangeImpl(adjustedText, changeEntry.change.getStart(), changeEntry.change.getEnd()); |
| insertionIndex = -1; |
| updateClientOffsetOnly = true; |
| continue; |
| } |
| |
| // Check if given change completely contains stored change range. |
| if (newChangeStart <= storedClientStart && newChangeEnd >= storedClientEnd) { |
| myChanges.remove(i); |
| insertionIndex = i; |
| newChangeEnd -= changeEntry.change.getDiff(); |
| i--; // Assuming that 'i' is incremented at the 'for' loop. |
| continue; |
| } |
| |
| // Check if given change intersects stored change range from the left. |
| if (newChangeStart <= storedClientStart && newChangeEnd > storedClientStart) { |
| int numberOfStoredChangeSymbolsToRemove = newChangeEnd - storedClientStart; |
| CharSequence adjustedText = storedText.subSequence(numberOfStoredChangeSymbolsToRemove, storedText.length()); |
| changeEntry.change = new TextChangeImpl(adjustedText, changeEntry.change.getStart(), changeEntry.change.getEnd()); |
| changeEntry.clientStartOffset += changeDiff + numberOfStoredChangeSymbolsToRemove; |
| 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()); |
| changeEntry.change = adjusted; |
| clientShift += adjusted.getDiff(); |
| newChangeEnd -= storedClientEnd - newChangeStart; |
| insertionIndex = i + 1; |
| continue; |
| } |
| |
| // Check if given change is left-adjacent to the stored change. |
| if (newChangeEnd == storedClientStart) { |
| changeEntry.clientStartOffset += changeDiff; |
| } |
| } |
| |
| 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.getClientEndOffset() == toMerge.clientStartOffset && 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.getClientEndOffset() == right.clientStartOffset && 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 { |
| changeIndex = -changeIndex - 1; |
| int clientShift = 0; |
| 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 StringFactory.createShared(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); |
| } |
| |
| @Override |
| public String toString() { |
| return myChanges.toString(); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * @return end offset of the current change at the 'client text', i.e. {@link #clientStartOffset} plus change text length |
| */ |
| public int getClientEndOffset() { |
| return clientStartOffset + change.getText().length(); |
| } |
| |
| @NonNls |
| @Override |
| public String toString() { |
| return "client start offset: " + clientStartOffset + ", change: " + change; |
| } |
| } |
| } |