blob: 28ffb3433eab519cb0cca5abca85987b64d6e53e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.netbeans.lib.editor.util;
/**
* Extension of the gap list which is expected to store elements
* that have an integer offset stored in them.
* <br>
* The elements must be stored in the list in an ascending order.
* <br>
* To efficiently manage offsets that need to change upon
* inserts and removes into an underlying storage (e.g. a swing document)
* the list maintains an offset gap that gets moved
* and shrinked/extended according to inserts/removals.
*
* <p>
* The physical raw offset stored in the element needs to be preprocessed
* to get the real offset value.
* <br>
* In short the raw offset is either the actual offset in case
* the offset gap is above it (greater or equal to <code>offsetGapStart</code> value)
* or, it's the actual offset plus the offset gap length otherwise.
*
* <p>
* Offsets up to +1GB (<code>Integer.MAX_VALUE / 2</code>)
* can be handled by this class which should be sufficient
* for most uses.
* <br>
* It's not +2GB as then the offsets shifted above the offset gap
* would overflow and be below zero which would break
* the comparisons whether the offset is below offset gap start.
* <br>
* Negative offsets are supported as well with a limit of -2GB.
*
* @author Miloslav Metelka
* @version 1.00
*/
public abstract class OffsetGapList<E> extends GapList<E> {
private int offsetGapStart; // 28 bytes (24-super + 4)
private int offsetGapLength = Integer.MAX_VALUE / 2; // 32 bytes
public OffsetGapList() {
this(10);
}
public OffsetGapList(int initialCapacity) {
super(initialCapacity);
}
/**
* Get the raw offset of the given element currently stored in the list.
*
* @param elem element currently stored in the list.
* @return raw offset of the element. It needs to be preprocessed
* by {@link #raw2Offset(int)} to become the real offset.
*/
protected abstract int elementRawOffset(E elem);
/**
* Set the raw offset of the given element currently stored in the list.
*
* @param elem element currently stored in the list.
* @param rawOffset raw offset to be stored in the given element.
*/
protected abstract void setElementRawOffset(E elem, int rawOffset);
/**
* Get the offset of the element stored in the list.
* <br>
* The raw offset stored in the element is preprocessed to become a real offset.
*
* @param elem element stored in the list.
* @return offset of the element.
*/
protected int elementOffset(E elem) {
return raw2Offset(elementRawOffset(elem));
}
/**
* Get the offset of the element stored in the list at the given index.
* <br>
* The raw offset stored in the element is preprocessed to become a real offset.
*
* @param index of the element in the list.
* @return offset of the element.
* @throws IndexOutOfBoundsException if index >= size() or lower than zero
*/
protected int elementOffset(int index) {
return elementOffset(get(index));
}
/**
* Inform the list that there was an insert done into an underlying storage
* (e.g. a swing document) which should move up offsets of the elements that have
* their offset greater or equal to the insertion offset.
*
* <p>
* Subclasses can build their own way of updating
* and they are not required to use this method.
*
* @param offset offset at which the insertion occurred.
* @param length length of the inserted area.
*/
public void defaultInsertUpdate(int offset, int length) {
assert (length >= 0);
if (offset != offsetGapStart()) {
moveOffsetGap(offset, findElementIndex(offset));
}
updateOffsetGapLength(-length); // Shrink the offset gap to simulate insertion
// Optimize for subsequent insert by moving gap start to the end
// of the just performed insertion.
// This way the subsequent insert will not need to call moveOffsetGap() at all.
// It's less optimal for insert-remove pairs (e.g. overwrite mode)
// but they should be less frequent.
updateOffsetGapStart(length);
}
/**
* Inform the list that there was a removal done into an underlying storage
* (e.g. a swing document) which should move down offsets of the elements
* that have their offsets greater than the removal offset.
* <br>
* The offsets inside the removal area will be moved to its begining.
* <p>
* Subclasses can build their own way of updating
* and they are not required to use this method.
*
* @param offset offset at which the removal occurred.
* @param length length of the removed area.
*/
public void defaultRemoveUpdate(int offset, int length) {
assert (length >= 0);
int index = findElementIndex(offset);
if (offset != offsetGapStart()) {
moveOffsetGap(offset, index);
}
int size = size();
int removeAreaEndRawOffset = offset + offsetGapLength + length;
// Move all elements inside the removed area to its end
// so that after update of the offset gap length they appear
// at the begining of the removal offset area
while (index < size) {
E elem = get(index++);
if (elementRawOffset(elem) < removeAreaEndRawOffset) {
setElementRawOffset(elem, removeAreaEndRawOffset);
} else { // all subsequent offsets are higher
break;
}
}
updateOffsetGapLength(+length);
}
/**
* Move the offset gap so that it's on the requested offset.
* <br>
* This method can be used when the index of the first element
* at the given offset was precomputed already.
*
* <p>
* <b>Note:</b> Improper use of this may logically damage
* offsets of the elements contained in the list.
*
* @param offset offset to which the <code>offsetGapStart</code>
* should be assigned.
* @param index index of the first element in the list
* that has an offset that is greater or equal that the given offset parameter.
* <br>
* It may be computed by {@link #findElementIndex(int)}.
*/
protected final void moveOffsetGap(int offset, int index) {
if (offset < offsetGapStart) { // need to check items above index
int bound = size();
for (int i = index; i < bound; i++) {
E elem = get(i);
int rawOffset = elementRawOffset(elem);
if (rawOffset < offsetGapStart) {
setElementRawOffset(elem, rawOffset + offsetGapLength);
} else {
break;
}
}
} else { // check items below index
for (int i = index - 1; i >= 0; i--) {
E elem = get(i);
int rawOffset = elementRawOffset(elem);
if (rawOffset >= offsetGapStart) {
setElementRawOffset(elem, rawOffset - offsetGapLength);
} else {
break;
}
}
}
offsetGapStart = offset;
}
/**
* Obtain the start of the offset gap.
*
* @return start of the offset gap.
*/
protected final int offsetGapStart() {
return offsetGapStart;
}
/**
* Update the offset gap start by the given delta.
* <br>
* This may be needed e.g. after insertion/removal was done
* in the document.
*
* <p>
* <b>Note:</b> Improper use of this may logically damage
* offsets of the elements contained in the list.
*/
protected final void updateOffsetGapStart(int offsetDelta) {
offsetGapStart += offsetDelta;
}
/**
* Obtain the length of the offset gap.
*
* @return length of the offset gap.
*/
protected final int offsetGapLength() {
return offsetGapLength;
}
/**
* Update the offset gap length by the given delta.
* <br>
* This may be needed e.g. after insertion/removal was done
* in the document.
*
* <p>
* <b>Note:</b> Improper use of this may logically damage
* offsets of the elements contained in the list.
*/
protected final void updateOffsetGapLength(int offsetGapLengthDelta) {
offsetGapLength += offsetGapLengthDelta;
assert (offsetGapLength >= 0); // prevent overflow to negative numbers
}
/**
* Find an index of the first element at the given offset in the list
* by using binary search.
*
* @param offset offset of the element
* @return index of the element. If there is no element with that
* index then the index of the next element (with the greater offset)
* (or size of the list) will be returned.
* <br>
* If there are multiple items with the same offset then the first one of them
* will be returned.
*/
protected final int findElementIndex(int offset) {
int low = 0;
int high = size() - 1;
while (low <= high) {
int index = (low + high) / 2; // mid in the binary search
int elemOffset = elementOffset(index);
if (elemOffset < offset) {
low = index + 1;
} else if (elemOffset > offset) {
high = index - 1;
} else { // exact offset found at index
while (index > 0) {
index--;
if (elementOffset(index) < offset) {
index++;
break;
}
}
low = index;
break;
}
}
return low;
}
/**
* This method updates element's offset (shifts it above offset gap if necessary)
* before adding the element to the list.
* <bt/>
* This method should be called before (or after) the element is physically added
* to the list. If the element is added below the offset gap
* then calling of this method is not necessary.
*/
protected void updateElementOffsetAdd(E elem) {
int rawOffset = elementRawOffset(elem);
if (rawOffset >= offsetGapStart) {
setElementRawOffset(elem, rawOffset + offsetGapLength);
}
}
/**
* This method updates element's offset (shifts it below offset gap if necessary)
* before (or after) the element gets removed from the list.
* <br/>
* This method should be called after the element is physically removed
* from the list and it's desired that it retains its natural offset
* (not possibly shifted by the offset gap length).
* <br/>
* If the element was located below the offset gap prior removal
* then calling of this method is not necessary.
*/
protected void updateElementOffsetRemove(E elem) {
int rawOffset = elementRawOffset(elem);
if (rawOffset >= offsetGapStart) {
setElementRawOffset(elem, rawOffset - offsetGapLength);
}
}
/**
* Translate raw offset into real offset.
*
* @param rawOffset raw offset stored in an element.
* @return real offset that the element is supposed to have.
*/
protected final int raw2Offset(int rawOffset) {
return (rawOffset < offsetGapStart)
? rawOffset
: rawOffset - offsetGapLength;
}
/**
* Translate regular offset to raw offset.
*
* @param offset regular offset.
* @return raw offset that can be used in elements.
*/
protected final int offset2raw(int offset) {
return (offset < offsetGapStart)
? offset
: offset + offsetGapLength;
}
protected void consistencyCheck() {
super.consistencyCheck();
if (offsetGapLength < 0) {
consistencyError("offsetGapLength < 0"); // NOI18N
}
int lastRawOffset = Integer.MIN_VALUE;
int lastOffset = Integer.MIN_VALUE;
int size = size();
for (int i = 0; i < size; i++) {
E elem = get(i);
int rawOffset = elementRawOffset(elem);
int offset = raw2Offset(rawOffset);
if (rawOffset < lastRawOffset) {
consistencyError("Invalid rawOffset=" // NOI18N
+ rawOffset + " >= lastRawOffset=" + lastRawOffset // NOI18N
+ " at index=" + i // NOI18N
);
}
if (offset < lastOffset) {
consistencyError("Invalid offset=" // NOI18N
+ offset + " >= lastOffset=" + lastOffset // NOI18N
+ " at index=" + i // NOI18N
);
}
lastRawOffset = rawOffset;
lastOffset = offset;
}
}
protected String dumpInternals() {
return super.dumpInternals() + ", offGap(s=" + offsetGapStart
+ ", l=" + offsetGapLength + ")";
}
}