/*******************************************************************************
* Copyright (c) 2012-2015 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.ext.java.jdt.text;
import org.eclipse.che.ide.api.editor.text.BadLocationException;
import org.eclipse.che.ide.api.editor.text.Region;
import org.eclipse.che.ide.api.editor.text.RegionImpl;
import org.eclipse.che.ide.ext.java.jdt.text.AbstractLineTracker.DelimiterInfo;
import org.eclipse.che.ide.runtime.Assert;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
/**
* Abstract implementation of <code>ILineTracker</code>. It lets the definition of line delimiters to subclasses. Assuming that
* '\n' is the only line delimiter, this abstract implementation defines the following line scheme:
* <ul>
* <li>"" -> [0,0]
* <li>"a" -> [0,1]
* <li>"\n" -> [0,1], [1,0]
* <li>"a\n" -> [0,2], [2,0]
* <li>"a\nb" -> [0,2], [2,1]
* <li>"a\nbc\n" -> [0,2], [2,3], [5,0]
* </ul>
* <p>
* This class must be subclassed.
* </p>
* <p>
* <strong>Performance:</strong> The query operations perform in <i>O(log n)</i> where <var>n</var> is the number of lines in the
* document. The modification operations roughly perform in <i>O(l * log n)</i> where <var>n</var> is the number of lines in the
* document and <var>l</var> is the sum of the number of removed, added or modified lines.
* </p>
*/
abstract class TreeLineTracker implements LineTracker {
/*
* Differential Balanced Binary Tree Assumption: lines cannot overlap => there exists a total ordering of the lines by their
* offset, which is the same as the ordering by line number Base idea: store lines in a binary search tree - the key is the
* line number / line offset -> lookup_line is O(log n) -> lookup_offset is O(log n) - a change in a line somewhere will change
* any succeeding line numbers / line offsets -> replace is O(n) Differential tree: instead of storing the key (line number,
* line offset) directly, every node stores the difference between its key and its parent's key - the sort key is still the
* line number / line offset, but it remains "virtual" - inserting a node (a line) really increases the virtual key of all
* succeeding nodes (lines), but this fact will not be realized in the key information encoded in the nodes. -> any change only
* affects the nodes in the node's parent chain, although more bookkeeping has to be done when changing a node or balancing the
* tree -> replace is O(log n) -> line offsets and line numbers have to be computed when walking the tree from the root / from
* a node -> still O(log n) The balancing algorithm chosen does not depend on the differential tree property. An AVL tree
* implementation has been chosen for simplicity.
*/
/*
* Turns assertions on/off. Don't make this a a debug option for performance reasons - this way the compiler can optimize the
* asserts away.
*/
private static final boolean ASSERT = false;
/** The empty delimiter of the last line. The last line and only the last line must have this zero-length delimiter. */
private static final String NO_DELIM = ""; //$NON-NLS-1$
/**
* A node represents one line. Its character and line offsets are 0-based and relative to the subtree covered by the node. All
* nodes under the left subtree represent lines before, all nodes under the right subtree lines after the current node.
*/
private static final class Node {
Node(int length, String delimiter) {
this.length = length;
this.delimiter = delimiter;
}
/** The line index in this node's line tree, or equivalently, the number of lines in the left subtree. */
int line;
/** The line offset in this node's line tree, or equivalently, the number of characters in the left subtree. */
int offset;
/** The number of characters in this line. */
int length;
/** The line delimiter of this line, needed to answer the delimiter query. */
String delimiter;
/** The parent node, <code>null</code> if this is the root node. */
Node parent;
/** The left subtree, possibly <code>null</code>. */
Node left;
/** The right subtree, possibly <code>null</code>. */
Node right;
/** The balance factor. */
byte balance;
/* @see java.lang.Object#toString() */
public final String toString() {
String bal;
switch (balance) {
case 0:
bal = "="; //$NON-NLS-1$
break;
case 1:
bal = "+"; //$NON-NLS-1$
break;
case 2:
bal = "++"; //$NON-NLS-1$
break;
case -1:
bal = "-"; //$NON-NLS-1$
break;
case -2:
bal = "--"; //$NON-NLS-1$
break;
default:
bal = Byte.toString(balance);
}
return "[" + offset + "+" + pureLength() + "+" + delimiter.length() + "|" + line + "|" + bal +
"]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
}
/**
* Returns the pure (without the line delimiter) length of this line.
*
* @return the pure line length
*/
int pureLength() {
return length - delimiter.length();
}
}
/** The root node of the tree, never <code>null</code>. */
private Node fRoot = new Node(0, NO_DELIM);
/** Creates a new line tracker. */
protected TreeLineTracker() {
}
/**
* Package visible constructor for creating a tree tracker from a list tracker.
*
* @param tracker
* the list line tracker
*/
TreeLineTracker(ListLineTracker tracker) {
final List<Line> lines = tracker.getLines();
final int n = lines.size();
if (n == 0)
return;
Line line = lines.get(0);
String delim = line.delimiter;
if (delim == null)
delim = NO_DELIM;
int length = line.length;
fRoot = new Node(length, delim);
Node node = fRoot;
for (int i = 1; i < n; i++) {
line = (Line)lines.get(i);
delim = line.delimiter;
if (delim == null)
delim = NO_DELIM;
length = line.length;
node = insertAfter(node, length, delim);
}
if (node.delimiter != NO_DELIM)
insertAfter(node, 0, NO_DELIM);
if (ASSERT)
checkTree();
}
/**
* Returns the node (line) including a certain offset. If the offset is between two lines, the line starting at
* <code>offset</code> is returned.
* <p>
* This means that for offsets smaller than the length, the following holds:
* </p>
* <p>
* <code>line.offset <= offset < line.offset + offset.length</code>.
* </p>
* <p>
* If <code>offset</code> is the document length, then this is true:
* </p>
* <p>
* <code>offset= line.offset + line.length</code>.
* </p>
*
* @param offset
* a document offset
* @return the line starting at or containing <code>offset</code>
* @throws BadLocationException
* if the offset is invalid
*/
private Node nodeByOffset(final int offset) throws BadLocationException {
/* Works for any binary search tree. */
int remaining = offset;
Node node = fRoot;
while (true) {
if (node == null)
fail(offset);
if (remaining < node.offset) {
node = node.left;
} else {
remaining -= node.offset;
if (remaining < node.length || remaining == node.length && node.right == null) { // last line
break;
}
remaining -= node.length;
node = node.right;
}
}
return node;
}
/**
* Returns the line number for the given offset. If the offset is between two lines, the line starting at <code>offset</code>
* is returned. The last line is returned if <code>offset</code> is equal to the document length.
*
* @param offset
* a document offset
* @return the line number starting at or containing <code>offset</code>
* @throws BadLocationException
* if the offset is invalid
*/
private int lineByOffset(final int offset) throws BadLocationException {
/* Works for any binary search tree. */
int remaining = offset;
Node node = fRoot;
int line = 0;
while (true) {
if (node == null)
fail(offset);
if (remaining < node.offset) {
node = node.left;
} else {
remaining -= node.offset;
line += node.line;
if (remaining < node.length || remaining == node.length && node.right == null) // last line
return line;
remaining -= node.length;
line++;
node = node.right;
}
}
}
/**
* Returns the node (line) with the given line number. Note that the last line is always incomplete, i.e. has the
* {@link #NO_DELIM} delimiter.
*
* @param line
* a line number
* @return the line with the given line number
* @throws BadLocationException
* if the line is invalid
*/
private Node nodeByLine(final int line) throws BadLocationException {
/* Works for any binary search tree. */
int remaining = line;
Node node = fRoot;
while (true) {
if (node == null)
fail(line);
if (remaining == node.line)
break;
if (remaining < node.line) {
node = node.left;
} else {
remaining -= node.line + 1;
node = node.right;
}
}
return node;
}
/**
* Returns the offset for the given line number. Note that the last line is always incomplete, i.e. has the {@link #NO_DELIM}
* delimiter.
*
* @param line
* a line number
* @return the line offset with the given line number
* @throws BadLocationException
* if the line is invalid
*/
private int offsetByLine(final int line) throws BadLocationException {
/* Works for any binary search tree. */
int remaining = line;
int offset = 0;
Node node = fRoot;
while (true) {
if (node == null)
fail(line);
if (remaining == node.line)
return offset + node.offset;
if (remaining < node.line) {
node = node.left;
} else {
remaining -= node.line + 1;
offset += node.offset + node.length;
node = node.right;
}
}
}
/**
* Left rotation - the given node is rotated down, its right child is rotated up, taking the previous structural position of
* <code>node</code>.
*
* @param node
* the node to rotate around
*/
private void rotateLeft(Node node) {
if (ASSERT)
Assert.isNotNull(node);
Node child = node.right;
if (ASSERT)
Assert.isNotNull(child);
boolean leftChild = node.parent == null || node == node.parent.left;
// restructure
setChild(node.parent, child, leftChild);
setChild(node, child.left, false);
setChild(child, node, true);
// update relative info
// child becomes the new parent, its line and offset counts increase as the former parent
// moves under child's left subtree
child.line += node.line + 1;
child.offset += node.offset + node.length;
}
/**
* Right rotation - the given node is rotated down, its left child is rotated up, taking the previous structural position of
* <code>node</code>.
*
* @param node
* the node to rotate around
*/
private void rotateRight(Node node) {
if (ASSERT)
Assert.isNotNull(node);
Node child = node.left;
if (ASSERT)
Assert.isNotNull(child);
boolean leftChild = node.parent == null || node == node.parent.left;
setChild(node.parent, child, leftChild);
setChild(node, child.right, true);
setChild(child, node, false);
// update relative info
// node loses its left subtree, except for what it keeps in its new subtree
// this is exactly the amount in child
node.line -= child.line + 1;
node.offset -= child.offset + child.length;
}
/**
* Helper method for moving a child, ensuring that parent pointers are set correctly.
*
* @param parent
* the new parent of <code>child</code>, <code>null</code> to replace the root node
* @param child
* the new child of <code>parent</code>, may be <code>null</code>
* @param isLeftChild
* <code>true</code> if <code>child</code> shall become <code>parent</code>'s left child, <code>false</code>
* if it shall become <code>parent</code>'s right child
*/
private void setChild(Node parent, Node child, boolean isLeftChild) {
if (parent == null) {
if (child == null)
fRoot = new Node(0, NO_DELIM);
else
fRoot = child;
} else {
if (isLeftChild)
parent.left = child;
else
parent.right = child;
}
if (child != null)
child.parent = parent;
}
/**
* A left rotation around <code>parent</code>, whose structural position is replaced by <code>node</code>.
*
* @param node
* the node moving up and left
* @param parent
* the node moving left and down
*/
private void singleLeftRotation(Node node, Node parent) {
rotateLeft(parent);
node.balance = 0;
parent.balance = 0;
}
/**
* A right rotation around <code>parent</code>, whose structural position is replaced by <code>node</code>.
*
* @param node
* the node moving up and right
* @param parent
* the node moving right and down
*/
private void singleRightRotation(Node node, Node parent) {
rotateRight(parent);
node.balance = 0;
parent.balance = 0;
}
/**
* A double left rotation, first rotating right around <code>node</code>, then left around <code>parent</code>.
*
* @param node
* the node that will be rotated right
* @param parent
* the node moving left and down
*/
private void rightLeftRotation(Node node, Node parent) {
Node child = node.left;
rotateRight(node);
rotateLeft(parent);
if (child.balance == 1) {
node.balance = 0;
parent.balance = -1;
child.balance = 0;
} else if (child.balance == 0) {
node.balance = 0;
parent.balance = 0;
} else if (child.balance == -1) {
node.balance = 1;
parent.balance = 0;
child.balance = 0;
}
}
/**
* A double right rotation, first rotating left around <code>node</code>, then right around <code>parent</code>.
*
* @param node
* the node that will be rotated left
* @param parent
* the node moving right and down
*/
private void leftRightRotation(Node node, Node parent) {
Node child = node.right;
rotateLeft(node);
rotateRight(parent);
if (child.balance == -1) {
node.balance = 0;
parent.balance = 1;
child.balance = 0;
} else if (child.balance == 0) {
node.balance = 0;
parent.balance = 0;
} else if (child.balance == 1) {
node.balance = -1;
parent.balance = 0;
child.balance = 0;
}
}
/**
* Inserts a line with the given length and delimiter after <code>node</code> .
*
* @param node
* the predecessor of the inserted node
* @param length
* the line length of the inserted node
* @param delimiter
* the delimiter of the inserted node
* @return the inserted node
*/
private Node insertAfter(Node node, int length, String delimiter) {
/*
* An insertion really shifts the key of all succeeding nodes. Hence we insert the added node between node and the successor
* of node. The added node becomes either the right child of the predecessor node, or the left child of the successor node.
*/
Node added = new Node(length, delimiter);
if (node.right == null)
setChild(node, added, false);
else
setChild(successorDown(node.right), added, true);
// parent chain update
updateParentChain(added, length, 1);
updateParentBalanceAfterInsertion(added);
return added;
}
/**
* Updates the balance information in the parent chain of node until it reaches the root or finds a node whose balance violates
* the AVL constraint, which is the re-balanced.
*
* @param node
* the child of the first node that needs balance updating
*/
private void updateParentBalanceAfterInsertion(Node node) {
Node parent = node.parent;
while (parent != null) {
if (node == parent.left)
parent.balance--;
else
parent.balance++;
switch (parent.balance) {
case 1:
case -1:
node = parent;
parent = node.parent;
continue;
case -2:
rebalanceAfterInsertionLeft(node);
break;
case 2:
rebalanceAfterInsertionRight(node);
break;
case 0:
break;
default:
if (ASSERT)
Assert.isTrue(false);
}
return;
}
}
/**
* Re-balances a node whose parent has a double positive balance.
*
* @param node
* the node to re-balance
*/
private void rebalanceAfterInsertionRight(Node node) {
Node parent = node.parent;
if (node.balance == 1) {
singleLeftRotation(node, parent);
} else if (node.balance == -1) {
rightLeftRotation(node, parent);
} else if (ASSERT) {
Assert.isTrue(false);
}
}
/**
* Re-balances a node whose parent has a double negative balance.
*
* @param node
* the node to re-balance
*/
private void rebalanceAfterInsertionLeft(Node node) {
Node parent = node.parent;
if (node.balance == -1) {
singleRightRotation(node, parent);
} else if (node.balance == 1) {
leftRightRotation(node, parent);
} else if (ASSERT) {
Assert.isTrue(false);
}
}
/*
* @see org.eclipse.jface.text.ILineTracker#replace(int, int, java.lang.String)
*/
public final void replace(int offset, int length, String text) throws BadLocationException {
if (ASSERT)
checkTree();
// Inlined nodeByOffset as we need both node and offset
int remaining = offset;
Node first = fRoot;
final int firstNodeOffset;
while (true) {
if (first == null)
fail(offset);
if (remaining < first.offset) {
first = first.left;
} else {
remaining -= first.offset;
if (remaining < first.length || remaining == first.length && first.right == null) { // last line
firstNodeOffset = offset - remaining;
break;
}
remaining -= first.length;
first = first.right;
}
}
// Inline nodeByOffset end
if (ASSERT)
Assert.isTrue(first != null);
Node last;
if (offset + length < firstNodeOffset + first.length)
last = first;
else
last = nodeByOffset(offset + length);
if (ASSERT)
Assert.isTrue(last != null);
int firstLineDelta = firstNodeOffset + first.length - offset;
if (first == last)
replaceInternal(first, text, length, firstLineDelta);
else
replaceFromTo(first, last, text, length, firstLineDelta);
if (ASSERT)
checkTree();
}
/**
* Replace happening inside a single line.
*
* @param node
* the affected node
* @param text
* the added text
* @param length
* the replace length, < <code>firstLineDelta</code>
* @param firstLineDelta
* the number of characters from the replacement offset to the end of <code>node</code> >
* <code>length</code>
*/
private void replaceInternal(Node node, String text, int length, int firstLineDelta) {
// 1) modification on a single line
DelimiterInfo info = text == null ? null : nextDelimiterInfo(text, 0);
if (info == null || info.delimiter == null) {
// a) trivial case: insert into a single node, no line mangling
int added = text == null ? 0 : text.length();
updateLength(node, added - length);
} else {
// b) more lines to add between two chunks of the first node
// remember what we split off the first line
int remainder = firstLineDelta - length;
String remDelim = node.delimiter;
// join the first line with the first added
int consumed = info.delimiterIndex + info.delimiterLength;
int delta = consumed - firstLineDelta;
updateLength(node, delta);
node.delimiter = info.delimiter;
// Inline addlines start
info = nextDelimiterInfo(text, consumed);
while (info != null) {
int lineLen = info.delimiterIndex - consumed + info.delimiterLength;
node = insertAfter(node, lineLen, info.delimiter);
consumed += lineLen;
info = nextDelimiterInfo(text, consumed);
}
// Inline addlines end
// add remaining chunk merged with last (incomplete) additional line
insertAfter(node, remainder + text.length() - consumed, remDelim);
}
}
/**
* Replace spanning from one node to another.
*
* @param node
* the first affected node
* @param last
* the last affected node
* @param text
* the added text
* @param length
* the replace length, >= <code>firstLineDelta</code>
* @param firstLineDelta
* the number of characters removed from the replacement offset to the end of <code>node</code>, <=
* <code>length</code>
*/
private void replaceFromTo(Node node, Node last, String text, int length, int firstLineDelta) {
// 2) modification covers several lines
// delete intermediate nodes
// TODO could be further optimized: replace intermediate lines with intermediate added lines
// to reduce re-balancing
Node successor = successor(node);
while (successor != last) {
length -= successor.length;
Node toDelete = successor;
successor = successor(successor);
updateLength(toDelete, -toDelete.length);
}
DelimiterInfo info = text == null ? null : nextDelimiterInfo(text, 0);
if (info == null || info.delimiter == null) {
int added = text == null ? 0 : text.length();
// join the two lines if there are no lines added
join(node, last, added - length);
} else {
// join the first line with the first added
int consumed = info.delimiterIndex + info.delimiterLength;
updateLength(node, consumed - firstLineDelta);
node.delimiter = info.delimiter;
length -= firstLineDelta;
// Inline addLines start
info = nextDelimiterInfo(text, consumed);
while (info != null) {
int lineLen = info.delimiterIndex - consumed + info.delimiterLength;
node = insertAfter(node, lineLen, info.delimiter);
consumed += lineLen;
info = nextDelimiterInfo(text, consumed);
}
// Inline addLines end
updateLength(last, text.length() - consumed - length);
}
}
/**
* Joins two consecutive node lines, additionally adjusting the resulting length of the combined line by <code>delta</code>.
* The first node gets deleted.
*
* @param one
* the first node to join
* @param two
* the second node to join
* @param delta
* the delta to apply to the remaining single node
*/
private void join(Node one, Node two, int delta) {
int oneLength = one.length;
updateLength(one, -oneLength);
updateLength(two, oneLength + delta);
}
/**
* Adjusts the length of a node by <code>delta</code>, also adjusting the parent chain of <code>node</code>. If the node's
* length becomes zero and is not the last (incomplete) node, it is deleted after the update.
*
* @param node
* the node to adjust
* @param delta
* the character delta to add to the node's length
*/
private void updateLength(Node node, int delta) {
if (ASSERT)
Assert.isTrue(node.length + delta >= 0);
// update the node itself
node.length += delta;
// check deletion
final int lineDelta;
boolean delete = node.length == 0 && node.delimiter != NO_DELIM;
if (delete)
lineDelta = -1;
else
lineDelta = 0;
// update parent chain
if (delta != 0 || lineDelta != 0)
updateParentChain(node, delta, lineDelta);
if (delete)
delete(node);
}
/**
* Updates the differential indices following the parent chain. All nodes from <code>from.parent</code> to the root are
* updated.
*
* @param node
* the child of the first node to update
* @param deltaLength
* the character delta
* @param deltaLines
* the line delta
*/
private void updateParentChain(Node node, int deltaLength, int deltaLines) {
updateParentChain(node, null, deltaLength, deltaLines);
}
/**
* Updates the differential indices following the parent chain. All nodes from <code>from.parent</code> to <code>to</code>
* (exclusive) are updated.
*
* @param from
* the child of the first node to update
* @param to
* the first node not to update
* @param deltaLength
* the character delta
* @param deltaLines
* the line delta
*/
private void updateParentChain(Node from, Node to, int deltaLength, int deltaLines) {
Node parent = from.parent;
while (parent != to) {
// only update node if update comes from left subtree
if (from == parent.left) {
parent.offset += deltaLength;
parent.line += deltaLines;
}
from = parent;
parent = from.parent;
}
}
/**
* Deletes a node from the tree, re-balancing it if necessary. The differential indices in the node's parent chain have to be
* updated in advance to calling this method. Generally, don't call <code>delete</code> directly, but call
* <code>update_length(node, -node.length)</code> to properly remove a node.
*
* @param node
* the node to delete.
*/
private void delete(Node node) {
if (ASSERT)
Assert.isTrue(node != null);
if (ASSERT)
Assert.isTrue(node.length == 0);
Node parent = node.parent;
Node toUpdate; // the parent of the node that lost a child
boolean lostLeftChild;
boolean isLeftChild = parent == null || node == parent.left;
if (node.left == null || node.right == null) {
// 1) node has one child at max - replace parent's pointer with the only child
// also handles the trivial case of no children
Node replacement = node.left == null ? node.right : node.left;
setChild(parent, replacement, isLeftChild);
toUpdate = parent;
lostLeftChild = isLeftChild;
// no updates to do - subtrees stay as they are
} else if (node.right.left == null) {
// 2a) node's right child has no left child - replace node with right child, giving node's
// left subtree to the right child
Node replacement = node.right;
setChild(parent, replacement, isLeftChild);
setChild(replacement, node.left, true);
replacement.line = node.line;
replacement.offset = node.offset;
replacement.balance = node.balance;
toUpdate = replacement;
lostLeftChild = false;
// } else if (node.left.right == null) {
// // 2b) symmetric case
// Node replacement= node.left;
// set_child(parent, replacement, isLeftChild);
// set_child(replacement, node.right, false);
// replacement.balance= node.balance;
// toUpdate= replacement;
// lostLeftChild= true;
} else {
// 3) hard case - replace node with its successor
Node successor = successor(node);
// successor exists (otherwise node would not have right child, case 1)
if (ASSERT)
Assert.isNotNull(successor);
// successor has no left child (a left child would be the real successor of node)
if (ASSERT)
Assert.isTrue(successor.left == null);
if (ASSERT)
Assert.isTrue(successor.line == 0);
// successor is the left child of its parent (otherwise parent would be smaller and
// hence the real successor)
if (ASSERT)
Assert.isTrue(successor == successor.parent.left);
// successor is not a child of node (would have been covered by 2a)
if (ASSERT)
Assert.isTrue(successor.parent != node);
toUpdate = successor.parent;
lostLeftChild = true;
// update relative indices
updateParentChain(successor, node, -successor.length, -1);
// delete successor from its current place - like 1)
setChild(toUpdate, successor.right, true);
// move node's subtrees to its successor
setChild(successor, node.right, false);
setChild(successor, node.left, true);
// replace node by successor in its parent
setChild(parent, successor, isLeftChild);
// update the successor
successor.line = node.line;
successor.offset = node.offset;
successor.balance = node.balance;
}
updateParentBalanceAfterDeletion(toUpdate, lostLeftChild);
}
/**
* Updates the balance information in the parent chain of node.
*
* @param node
* the first node that needs balance updating
* @param wasLeftChild
* <code>true</code> if the deletion happened on <code>node</code>'s left subtree, <code>false</code> if it
* occurred on <code>node</code>'s right subtree
*/
private void updateParentBalanceAfterDeletion(Node node, boolean wasLeftChild) {
while (node != null) {
if (wasLeftChild)
node.balance++;
else
node.balance--;
Node parent = node.parent;
if (parent != null)
wasLeftChild = node == parent.left;
switch (node.balance) {
case 1:
case -1:
return; // done, no tree change
case -2:
if (rebalanceAfterDeletionRight(node.left))
return;
break; // propagate up
case 2:
if (rebalanceAfterDeletionLeft(node.right))
return;
break; // propagate up
case 0:
break; // propagate up
default:
if (ASSERT)
Assert.isTrue(false);
}
node = parent;
}
}
/**
* Re-balances a node whose parent has a double positive balance.
*
* @param node
* the node to re-balance
* @return <code>true</code> if the re-balancement leaves the height at <code>node.parent</code> constant, <code>false</code>
* if the height changed
*/
private boolean rebalanceAfterDeletionLeft(Node node) {
Node parent = node.parent;
if (node.balance == 1) {
singleLeftRotation(node, parent);
return false;
} else if (node.balance == -1) {
rightLeftRotation(node, parent);
return false;
} else if (node.balance == 0) {
rotateLeft(parent);
node.balance = -1;
parent.balance = 1;
return true;
} else {
if (ASSERT)
Assert.isTrue(false);
return true;
}
}
/**
* Re-balances a node whose parent has a double negative balance.
*
* @param node
* the node to re-balance
* @return <code>true</code> if the re-balancement leaves the height at <code>node.parent</code> constant, <code>false</code>
* if the height changed
*/
private boolean rebalanceAfterDeletionRight(Node node) {
Node parent = node.parent;
if (node.balance == -1) {
singleRightRotation(node, parent);
return false;
} else if (node.balance == 1) {
leftRightRotation(node, parent);
return false;
} else if (node.balance == 0) {
rotateRight(parent);
node.balance = 1;
parent.balance = -1;
return true;
} else {
if (ASSERT)
Assert.isTrue(false);
return true;
}
}
/**
* Returns the successor of a node, <code>null</code> if node is the last node.
*
* @param node
* a node
* @return the successor of <code>node</code>, <code>null</code> if there is none
*/
private Node successor(Node node) {
if (node.right != null)
return successorDown(node.right);
return successorUp(node);
}
/**
* Searches the successor of <code>node</code> in its parent chain.
*
* @param node
* a node
* @return the first node in <code>node</code>'s parent chain that is reached from its left subtree, <code>null</code> if there
* is none
*/
private Node successorUp(final Node node) {
Node child = node;
Node parent = child.parent;
while (parent != null) {
if (child == parent.left)
return parent;
child = parent;
parent = child.parent;
}
if (ASSERT)
Assert.isTrue(node.delimiter == NO_DELIM);
return null;
}
/**
* Searches the left-most node in a given subtree.
*
* @param node
* a node
* @return the left-most node in the given subtree
*/
private Node successorDown(Node node) {
Node child = node.left;
while (child != null) {
node = child;
child = node.left;
}
return node;
}
/* miscellaneous */
/**
* Throws an exception.
*
* @param offset
* the illegal character or line offset that caused the exception
* @throws org.eclipse.che.ide.api.editor.text.BadLocationException
* always
*/
private void fail(int offset) throws BadLocationException {
throw new BadLocationException();
}
/**
* Returns the information about the first delimiter found in the given text starting at the given offset.
*
* @param text
* the text to be searched
* @param offset
* the offset in the given text
* @return the information of the first found delimiter or <code>null</code>
*/
protected abstract DelimiterInfo nextDelimiterInfo(String text, int offset);
/* @see org.eclipse.jface.text.ILineTracker#getLineDelimiter(int) */
public final String getLineDelimiter(int line) throws BadLocationException {
Node node = nodeByLine(line);
return node.delimiter == NO_DELIM ? null : node.delimiter;
}
/*
* @see org.eclipse.jface.text.ILineTracker#computeNumberOfLines(java.lang.String)
*/
public final int computeNumberOfLines(String text) {
int count = 0;
int start = 0;
DelimiterInfo delimiterInfo = nextDelimiterInfo(text, start);
while (delimiterInfo != null && delimiterInfo.delimiterIndex > -1) {
++count;
start = delimiterInfo.delimiterIndex + delimiterInfo.delimiterLength;
delimiterInfo = nextDelimiterInfo(text, start);
}
return count;
}
/* @see org.eclipse.jface.text.ILineTracker#getNumberOfLines() */
public final int getNumberOfLines() {
// TODO track separately?
Node node = fRoot;
int lines = 0;
while (node != null) {
lines += node.line + 1;
node = node.right;
}
return lines;
}
/* @see org.eclipse.jface.text.ILineTracker#getNumberOfLines(int, int) */
public final int getNumberOfLines(int offset, int length) throws BadLocationException {
if (length == 0)
return 1;
int startLine = lineByOffset(offset);
int endLine = lineByOffset(offset + length);
return endLine - startLine + 1;
}
/* @see org.eclipse.jface.text.ILineTracker#getLineOffset(int) */
public final int getLineOffset(int line) throws BadLocationException {
return offsetByLine(line);
}
/* @see org.eclipse.jface.text.ILineTracker#getLineLength(int) */
public final int getLineLength(int line) throws BadLocationException {
Node node = nodeByLine(line);
return node.length;
}
/* @see org.eclipse.jface.text.ILineTracker#getLineNumberOfOffset(int) */
public final int getLineNumberOfOffset(int offset) throws BadLocationException {
return lineByOffset(offset);
}
/* @see org.eclipse.jface.text.ILineTracker#getLineInformationOfOffset(int) */
public final Region getLineInformationOfOffset(final int offset) throws BadLocationException {
// Inline nodeByOffset start as we need both node and offset
int remaining = offset;
Node node = fRoot;
final int lineOffset;
while (true) {
if (node == null)
fail(offset);
if (remaining < node.offset) {
node = node.left;
} else {
remaining -= node.offset;
if (remaining < node.length || remaining == node.length && node.right == null) { // last line
lineOffset = offset - remaining;
break;
}
remaining -= node.length;
node = node.right;
}
}
// Inline nodeByOffset end
return new RegionImpl(lineOffset, node.pureLength());
}
/* @see org.eclipse.jface.text.ILineTracker#getLineInformation(int) */
public final Region getLineInformation(int line) throws BadLocationException {
try {
// Inline nodeByLine start
int remaining = line;
int offset = 0;
Node node = fRoot;
while (true) {
if (node == null)
fail(line);
if (remaining == node.line) {
offset += node.offset;
break;
}
if (remaining < node.line) {
node = node.left;
} else {
remaining -= node.line + 1;
offset += node.offset + node.length;
node = node.right;
}
}
// Inline nodeByLine end
return new RegionImpl(offset, node.pureLength());
} catch (BadLocationException x) {
/*
* FIXME: this really strange behavior is mandated by the previous line tracker implementation and included here for
* compatibility. See LineTrackerTest3#testFunnyLastLineCompatibility().
*/
if (line > 0 && line == getNumberOfLines()) {
line = line - 1;
// Inline nodeByLine start
int remaining = line;
int offset = 0;
Node node = fRoot;
while (true) {
if (node == null)
fail(line);
if (remaining == node.line) {
offset += node.offset;
break;
}
if (remaining < node.line) {
node = node.left;
} else {
remaining -= node.line + 1;
offset += node.offset + node.length;
node = node.right;
}
}
Node last = node;
// Inline nodeByLine end
if (last.length > 0)
return new RegionImpl(offset + last.length, 0);
}
throw x;
}
}
/* @see org.eclipse.jface.text.ILineTracker#set(java.lang.String) */
public final void set(String text) {
fRoot = new Node(0, NO_DELIM);
try {
replace(0, 0, text);
} catch (BadLocationException x) {
throw new RuntimeException();
}
}
/* @see java.lang.Object#toString() */
public String toString() {
int depth = computeDepth(fRoot);
int WIDTH = 30;
int leaves = (int)Math.pow(2, depth - 1);
int width = WIDTH * leaves;
String empty = "."; //$NON-NLS-1$
List<Node> roots = new LinkedList<TreeLineTracker.Node>();
roots.add(fRoot);
StringBuffer buf = new StringBuffer((width + 1) * depth);
int indents = leaves;
char[] space = new char[leaves * WIDTH / 2];
Arrays.fill(space, ' ');
for (int d = 0; d < depth; d++) {
// compute indent
indents /= 2;
int spaces = Math.max(0, indents * WIDTH - WIDTH / 2);
// print nodes
for (ListIterator<Node> it = roots.listIterator(); it.hasNext(); ) {
// pad before
buf.append(space, 0, spaces);
Node node = it.next();
String box;
// replace the node with its children
if (node == null) {
it.add(null);
box = empty;
} else {
it.set(node.left);
it.add(node.right);
box = node.toString();
}
// draw the node, pad to WIDTH
int pad_left = (WIDTH - box.length() + 1) / 2;
int pad_right = WIDTH - box.length() - pad_left;
buf.append(space, 0, pad_left);
buf.append(box);
buf.append(space, 0, pad_right);
// pad after
buf.append(space, 0, spaces);
}
buf.append('\n');
}
return buf.toString();
}
/**
* Recursively computes the depth of the tree. Only used by {@link #toString()}.
*
* @param root
* the subtree to compute the depth of, may be <code>null</code>
* @return the depth of the given tree, 0 if it is <code>null</code>
*/
private byte computeDepth(Node root) {
if (root == null)
return 0;
return (byte)(Math.max(computeDepth(root.left), computeDepth(root.right)) + 1);
}
/** Debug-only method that checks the tree structure and the differential offsets. */
private void checkTree() {
checkTreeStructure(fRoot);
try {
checkTreeOffsets(nodeByOffset(0), new int[]{0, 0}, null);
} catch (BadLocationException x) {
throw new AssertionError();
}
}
/**
* Debug-only method that validates the tree structure below <code>node</code>. I.e. it checks whether all parent/child
* pointers are consistent and whether the AVL balance information is correct.
*
* @param node
* the node to validate
* @return the depth of the tree under <code>node</code>
*/
private byte checkTreeStructure(Node node) {
if (node == null)
return 0;
byte leftDepth = checkTreeStructure(node.left);
byte rightDepth = checkTreeStructure(node.right);
Assert.isTrue(node.balance == rightDepth - leftDepth);
Assert.isTrue(node.left == null || node.left.parent == node);
Assert.isTrue(node.right == null || node.right.parent == node);
return (byte)(Math.max(rightDepth, leftDepth) + 1);
}
/**
* Debug-only method that checks the differential offsets of the tree, starting at <code>node</code> and continuing until
* <code>last</code>.
*
* @param node
* the first <code>Node</code> to check, may be <code>null</code>
* @param offLen
* an array of length 2, with <code>offLen[0]</code> the expected offset of <code>node</code> and
* <code>offLen[1]</code> the expected line of <code>node</code>
* @param last
* the last <code>Node</code> to check, may be <code>null</code>
* @return an <code>int[]</code> of length 2, with the first element being the character length of <code>node</code>'s subtree,
* and the second element the number of lines in <code>node</code>'s subtree
*/
private int[] checkTreeOffsets(Node node, int[] offLen, Node last) {
if (node == last)
return offLen;
Assert.isTrue(node.offset == offLen[0]);
Assert.isTrue(node.line == offLen[1]);
if (node.right != null) {
int[] result = checkTreeOffsets(successorDown(node.right), new int[2], node);
offLen[0] += result[0];
offLen[1] += result[1];
}
offLen[0] += node.length;
offLen[1]++;
return checkTreeOffsets(node.parent, offLen, last);
}
}