/*
* Copyright (c) 2012 Sam Harwell, Tunnel Vision Laboratories LLC
* All rights reserved.
*
* The source code of this document is proprietary work, and is not licensed for
* distribution. For information about licensing, contact Sam Harwell at:
* sam@tunnelvisionlabs.com
*/
package org.antlr.netbeans.editor.text.impl;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import org.antlr.netbeans.editor.text.DocumentChange;
import org.antlr.netbeans.editor.text.NormalizedDocumentChangeCollection;
import org.netbeans.api.annotations.common.NonNull;
import org.openide.util.Parameters;
/**
*
* @author Sam Harwell
*/
final class NbNormalizedDocumentChangeCollection extends AbstractList<DocumentChange> implements NormalizedDocumentChangeCollection {
private final List<NbDocumentChange> internal = new ArrayList<>();
private boolean includesLineChanges;
private boolean readOnly;
void freeze() {
readOnly = true;
}
@Override
public boolean getIncludesLineChanges() {
return includesLineChanges;
}
@Override
public boolean isReadOnly() {
return readOnly;
}
@Override
public void add(int index, DocumentChange element) {
Parameters.notNull("element", element);
if (isReadOnly()) {
throw new UnsupportedOperationException();
}
if (!(element instanceof NbDocumentChange)) {
throw new UnsupportedOperationException();
}
add(index, (NbDocumentChange)element);
}
public void add(int index, @NonNull NbDocumentChange element) {
Parameters.notNull("element", element);
if (isReadOnly()) {
throw new UnsupportedOperationException();
}
if (element.getOldOffset() != element.getNewOffset()) {
throw new UnsupportedOperationException("Move changes are not yet supported.");
}
if (element.getLineCountDelta() != 0) {
includesLineChanges = true;
}
int mergeIndex = -1;
int lastMergeIndex = -1;
for (index = 0; index < internal.size() && element.getOldRegion().getEnd() >= internal.get(index).getNewOffset(); index++) {
if (element.getOldRegion().intersectsWith(internal.get(index).getNewRegion())) {
lastMergeIndex = index;
if (mergeIndex < 0) {
mergeIndex = index;
}
} else if (mergeIndex >= 0) {
break;
}
}
if (mergeIndex >= 0) {
NbDocumentChange merged = mergeChanges(element, internal.subList(mergeIndex, lastMergeIndex + 1));
internal.set(mergeIndex, merged);
internal.subList(mergeIndex + 1, lastMergeIndex + 1).clear();
index = mergeIndex;
} else {
internal.add(index, adjustOldState(element, internal.subList(0, index)));
}
for (int i = index + 1; i < internal.size(); i++) {
// always adjust the remaining ones based on the original update
internal.set(i, adjustNewState(internal.get(i), element));
}
assert repOk();
}
@Override
public DocumentChange get(int index) {
return internal.get(index);
}
@Override
public int size() {
return internal.size();
}
private @NonNull NbDocumentChange mergeChanges(@NonNull NbDocumentChange update, @NonNull List<NbDocumentChange> existing) {
Parameters.notNull("update", update);
Parameters.notNull("existing", existing);
if (existing.isEmpty()) {
throw new IllegalArgumentException();
}
if (update.getOldOffset() != update.getNewOffset()) {
throw new UnsupportedOperationException("Move operations are not supported.");
}
NbDocumentChange firstChange = existing.get(0);
NbDocumentChange lastChange = existing.get(existing.size() - 1);
int firstChangeOffset = firstChange.getNewOffset() - firstChange.getOldOffset();
int lastChangeOffset = lastChange.getNewOffset() - lastChange.getOldOffset();
int updateOriginalStart =
update.getOldOffset() < firstChange.getNewOffset()
? update.getOldOffset() - firstChangeOffset
: firstChange.getOldOffset();
int updateOriginalEnd =
update.getOldOffset() + update.getOldLength() > lastChange.getNewOffset() + lastChange.getNewLength()
? update.getOldOffset() + update.getOldLength() - lastChangeOffset - lastChange.getDelta()
: lastChange.getOldOffset() + lastChange.getOldLength();
int oldPosition = Math.min(updateOriginalStart, firstChange.getOldOffset());
int oldEndPosition = Math.max(updateOriginalEnd, lastChange.getOldOffset() + lastChange.getOldLength());
int newPosition = oldPosition + firstChangeOffset;
int newEndPosition = oldEndPosition + lastChangeOffset + lastChange.getDelta() + update.getDelta();
StringBuilder oldText = new StringBuilder(oldEndPosition - oldPosition);
oldText.setLength(oldEndPosition - oldPosition);
StringBuilder newText = new StringBuilder(newEndPosition - newPosition);
newText.setLength(newEndPosition - newPosition);
// fill the oldText buffer
int fillPoint = 0;
NbDocumentChange previousChange = null;
for (int i = 0; i <= existing.size(); i++) {
NbDocumentChange change = (i < existing.size()) ? existing.get(i) : null;
int rawStart = change != null ? change.getOldOffset() : oldEndPosition;
int rawEnd = change != null ? change.getOldOffset() + change.getOldLength() : oldEndPosition;
int start = rawStart - oldPosition;
int end = rawEnd - oldPosition;
if (start > fillPoint) {
// pull the intermediate characters from the update change
if (previousChange == null) {
oldText.replace(0, start, update.getOldText().substring(0, start));
} else {
int a = previousChange.getNewOffset() + previousChange.getNewLength() - update.getOldOffset();
int b = a + (start - fillPoint);
oldText.replace(fillPoint, start, update.getOldText().substring(a, b));
}
}
if (start < end) {
assert change != null : "Should have a change.";
oldText.replace(start, end, change.getOldText());
}
fillPoint = end;
previousChange = change;
}
// fill the newText buffer
newText.replace(update.getNewOffset() - newPosition, update.getNewOffset() + update.getNewLength() - newPosition, update.getNewText());
if (newPosition < update.getNewOffset()) {
int start = 0;
int end = update.getNewOffset() - newPosition;
newText.replace(start, end, firstChange.getNewText().substring(0, end));
}
if (newEndPosition > update.getNewOffset() + update.getNewLength()) {
int start = update.getNewOffset() + update.getNewLength() - newPosition;
int end = newEndPosition - newPosition;
newText.replace(start, end, lastChange.getNewText().substring(lastChange.getNewLength() - (end - start), lastChange.getNewLength()));
}
return new NbDocumentChange(oldPosition, oldText.toString(), newPosition, newText.toString());
}
private boolean repOk() {
for (int i = 0; i < internal.size() - 1; i++) {
NbDocumentChange current = internal.get(i);
NbDocumentChange next = internal.get(i + 1);
if (current.getOldOffset() + current.getOldLength() > next.getOldOffset()) {
return false;
}
if (current.getNewOffset() + current.getNewLength() > next.getNewOffset()) {
return false;
}
if (current.getOldRegion().intersectsWith(next.getOldRegion())) {
return false;
}
if (current.getNewRegion().intersectsWith(next.getNewRegion())) {
return false;
}
}
return true;
}
// /**
// * Attempts to merge two intersecting changes. If the changes are disjoint,
// * this method returns {@code null}.
// * <p>
// * If {@code linked} is {@code true}, the first change is applied, and then the
// * second change is applied to the result.
// *
// * <p>
// * If {@code linked} is {@code false}, the first change is assumed to invalidate
// * any overlapping region of the second. In other words, the target state of
// * the first change is newer than the target state of the second change.
// *
// * @param linked <c>true</c> if the second change is applied to the new state of the first;
// * <c>false</c> if the second change is applied to the old state of the first (e.g. as the
// * result of a previous merge).
// *
// * @return An {@link NbDocumentChange} if the changes are successfully merged, else
// * {@code false} if the changes are disjoint.
// */
// private NbDocumentChange mergeChanges(NbDocumentChange first, NbDocumentChange second, boolean linked) {
// Parameters.notNull("first", first);
// Parameters.notNull("second", second);
//
// OffsetRegion firstSpan = linked ? first.getNewSpan() : first.getOldSpan();
// OffsetRegion secondSpan = second.getOldSpan();
// if (!firstSpan.intersectsWith(secondSpan)) {
// return null;
// }
//
// int oldPosition;
// String oldText;
// int newPosition;
// String newText;
//
// if (linked) {
// if (first.getOldPosition() < second.getOldPosition()) {
// oldPosition = first.getOldPosition();
// newPosition = first.getNewPosition();
// } else {
// oldPosition = second.getOldPosition();
// newPosition = second.getNewPosition();
// }
// } else {
// if (first.getOldPosition() < second.getOldPosition()) {
// oldPosition = first.getOldPosition();
// newPosition = first.getNewPosition();
// oldText = first.getOldText();
// if (!first.getOldSpan().contains(second.getOldSpan())) {
// oldText += second.getOldText().substring(first.getOldSpan().getEnd() - second.getOldSpan().getStart(), second.getOldSpan().getEnd());
// }
// } else {
// oldPosition = second.getOldPosition();
// newPosition = second.getNewPosition();
// oldText = second.getOldText();
// if (!second.getOldSpan().contains(first.getOldSpan())) {
// oldText += first.getOldText().substring(second.getOldSpan().getEnd() - first.getOldSpan().getStart(), first.getOldSpan().getEnd());
// }
// }
// }
//
// return new NbDocumentChange(oldPosition, oldText, newPosition, newText);
// }
//
// private NbDocumentChange mergeChanges(NbDocumentChange first, NbDocumentChange second) {
// Parameters.notNull("first", first);
// Parameters.notNull("second", second);
//
// OffsetRegion firstSpan = linked ? first.getNewSpan() : first.getOldSpan();
// OffsetRegion secondSpan = second.getOldSpan();
// if (!firstSpan.intersectsWith(secondSpan)) {
// return null;
// }
//
// int oldPosition;
// String oldText;
// int newPosition;
// String newText;
//
// if (linked) {
// if (first.getOldPosition() < second.getOldPosition()) {
// oldPosition = first.getOldPosition();
// newPosition = first.getNewPosition();
// } else {
// oldPosition = second.getOldPosition();
// newPosition = second.getNewPosition();
// }
// } else {
// if (first.getOldPosition() < second.getOldPosition()) {
// oldPosition = first.getOldPosition();
// newPosition = first.getNewPosition();
// oldText = first.getOldText();
// if (!first.getOldSpan().contains(second.getOldSpan())) {
// oldText += second.getOldText().substring(first.getOldSpan().getEnd() - second.getOldSpan().getStart(), second.getOldSpan().getEnd());
// }
// } else {
// oldPosition = second.getOldPosition();
// newPosition = second.getNewPosition();
// oldText = second.getOldText();
// if (!second.getOldSpan().contains(first.getOldSpan())) {
// oldText += first.getOldText().substring(second.getOldSpan().getEnd() - first.getOldSpan().getStart(), first.getOldSpan().getEnd());
// }
// }
// }
//
// return new NbDocumentChange(oldPosition, oldText, newPosition, newText);
// }
private @NonNull NbDocumentChange adjustOldState(@NonNull NbDocumentChange element, @NonNull Iterable<NbDocumentChange> changes) {
Parameters.notNull("element", element);
Parameters.notNull("changes", changes);
int delta = 0;
for (NbDocumentChange change : changes) {
assert !element.getOldRegion().intersectsWith(change.getNewRegion()) : "The changes should not intersect.";
if (change.getNewOffset() <= element.getOldOffset()) {
delta += change.getDelta();
}
}
if (delta == 0) {
return element;
}
return new NbDocumentChange(element.getOldOffset() - delta, element.getOldText(), element.getNewOffset(), element.getNewText(), element.getLineCountDelta());
}
private @NonNull NbDocumentChange adjustNewState(@NonNull NbDocumentChange element, @NonNull NbDocumentChange change) {
Parameters.notNull("element", element);
Parameters.notNull("change", change);
assert !element.getNewRegion().intersectsWith(change.getOldRegion()) : "The changes should not intersect.";
if (change.getDelta() == 0 || change.getOldOffset() > element.getNewOffset()) {
return element;
}
return new NbDocumentChange(element.getOldOffset(), element.getOldText(), element.getNewOffset() + change.getDelta(), element.getNewText(), element.getLineCountDelta());
}
}