diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/commons/ILineTracker.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/commons/ILineTracker.java new file mode 100644 index 000000000..09941f2c9 --- /dev/null +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/commons/ILineTracker.java @@ -0,0 +1,150 @@ +/******************************************************************************* + * Copyright (c) 2000, 2009 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.lsp4xml.commons; +//package org.eclipse.jface.text; + +import org.eclipse.lsp4j.Position; + +/** + * A line tracker maps character positions to line numbers and vice versa. + * Initially the line tracker is informed about its underlying text in order to + * initialize the mapping information. After that, the line tracker is informed + * about all changes of the underlying text allowing for incremental updates of + * the mapping information. It is the client's responsibility to actively inform + * the line tacker about text changes. For example, when using a line tracker in + * combination with a document the document controls the line tracker. + *
+ * In order to provide backward compatibility for clients of ILineTracker
, extension
+ * interfaces are used to provide a means of evolution. The following extension interfaces
+ * exist:
+ *
+ * Clients may implement this interface or use the standard implementation + *
+ * {@link org.eclipse.jface.text.DefaultLineTracker}or + * {@link org.eclipse.jface.text.ConfigurableLineTracker}. + */ +public interface ILineTracker { + + /** + * Returns the line delimiter of the specified line. Returnsnull
if the
+ * line is not closed with a line delimiter.
+ *
+ * @param line the line whose line delimiter is queried
+ * @return the line's delimiter or null
if line does not have a delimiter
+ * @exception BadLocationException if the line number is invalid in this tracker's line structure
+ */
+ String getLineDelimiter(int line) throws BadLocationException;
+
+ /**
+ * Computes the number of lines in the given text.
+ *
+ * @param text the text whose number of lines should be computed
+ * @return the number of lines in the given text
+ */
+ int computeNumberOfLines(String text);
+
+ /**
+ * Returns the number of lines.
+ * + * Note that a document always has at least one line. + *
+ * + * @return the number of lines in this tracker's line structure + */ + int getNumberOfLines(); + + /** + * Returns the number of lines which are occupied by a given text range. + * + * @param offset the offset of the specified text range + * @param length the length of the specified text range + * @return the number of lines occupied by the specified range + * @exception BadLocationException if specified range is unknown to this tracker + */ + int getNumberOfLines(int offset, int length) throws BadLocationException; + + /** + * Returns the position of the first character of the specified line. + * + * @param line the line of interest + * @return offset of the first character of the line + * @exception BadLocationException if the line is unknown to this tracker + */ + int getLineOffset(int line) throws BadLocationException; + + /** + * Returns length of the specified line including the line's delimiter. + * + * @param line the line of interest + * @return the length of the line + * @exception BadLocationException if line is unknown to this tracker + */ + int getLineLength(int line) throws BadLocationException; + + /** + * Returns the line number the character at the given offset belongs to. + * + * @param offset the offset whose line number to be determined + * @return the number of the line the offset is on + * @exception BadLocationException if the offset is invalid in this tracker + */ + int getLineNumberOfOffset(int offset) throws BadLocationException; + + /** + * Returns a line description of the line at the given offset. + * The description contains the start offset and the length of the line + * excluding the line's delimiter. + * + * @param offset the offset whose line should be described + * @return a region describing the line + * @exception BadLocationException if offset is invalid in this tracker + */ + Line getLineInformationOfOffset(int offset) throws BadLocationException; + + /** + * Returns a line description of the given line. The description + * contains the start offset and the length of the line excluding the line's + * delimiter. + * + * @param line the line that should be described + * @return a region describing the line + * @exception BadLocationException if line is unknown to this tracker + */ + Line getLineInformation(int line) throws BadLocationException; + + /** + * Informs the line tracker about the specified change in the tracked text. + * + * @param offset the offset of the replaced text + * @param length the length of the replaced text + * @param text the substitution text + * @exception BadLocationException if specified range is unknown to this tracker + */ + void replace(int offset, int length, String text) throws BadLocationException; + + /** + * Sets the tracked text to the specified text. + * + * @param text the new tracked text + */ + void set(String text); + + Position getPositionAt(int position) throws BadLocationException; + + int getOffsetAt(Position position) throws BadLocationException; +} \ No newline at end of file diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/commons/ListLineTracker.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/commons/ListLineTracker.java index 41dae15d3..3c63ea93e 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/commons/ListLineTracker.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/commons/ListLineTracker.java @@ -33,7 +33,7 @@ * * @since 3.2 */ -class ListLineTracker /* implements ILineTracker */ { +class ListLineTracker implements ILineTracker { /** The predefined delimiters of this tracker */ public final static String[] DELIMITERS = { "\r", "\n", "\r\n" }; //$NON-NLS-3$ //$NON-NLS-1$ //$NON-NLS-2$ @@ -104,6 +104,7 @@ private int findLine(int offset) { return left; } + @Override public final Position getPositionAt(int offset) throws BadLocationException { int lineNumber = getLineNumberOfOffset(offset); int lines = fLines.size(); @@ -120,7 +121,53 @@ public final Position getPositionAt(int offset) throws BadLocationException { return new Position(lineNumber, character); } - private final int getLineNumberOfOffset(int position) throws BadLocationException { + + /** + * Returns the number of lines covered by the specified text range. + * + * @param startLine the line where the text range starts + * @param offset the start offset of the text range + * @param length the length of the text range + * @return the number of lines covered by this text range + * @exception BadLocationException if range is undefined in this tracker + */ + private int getNumberOfLines(int startLine, int offset, int length) throws BadLocationException { + + if (length == 0) + return 1; + + int target= offset + length; + + Line l= fLines.get(startLine); + + if (l.delimiter == null) + return 1; + + if (l.offset + l.length > target) + return 1; + + if (l.offset + l.length == target) + return 2; + + return getLineNumberOfOffset(target) - startLine + 1; + } + + @Override + public final int getLineLength(int line) throws BadLocationException { + int lines= fLines.size(); + + if (line < 0 || line > lines) + throw new BadLocationException(); + + if (lines == 0 || lines == line) + return 0; + + Line l= fLines.get(line); + return l.length; + } + + @Override + public final int getLineNumberOfOffset(int position) throws BadLocationException { if (position < 0) { throw new BadLocationException("Negative offset : " + position); //$NON-NLS-1$ } else if (position > fTextLength) { @@ -140,6 +187,7 @@ private final int getLineNumberOfOffset(int position) throws BadLocationExceptio return findLine(position); } + @Override public int getOffsetAt(Position position) throws BadLocationException { int line = position.getLine(); int lines = fLines.size(); @@ -172,6 +220,23 @@ public int getOffsetAt(Position position) throws BadLocationException { return offset; } + @Override + public final Line getLineInformationOfOffset(int position) throws BadLocationException { + if (position > fTextLength) + throw new BadLocationException("Offset > length: " + position + " > " + fTextLength); //$NON-NLS-1$//$NON-NLS-2$ + + if (position == fTextLength) { + int size= fLines.size(); + if (size == 0) + return new Line(0, 0); + Line l= fLines.get(size - 1); + return (l.delimiter != null ? new Line(fTextLength, 0) : new Line(fTextLength - l.length, l.length)); + } + + return getLineInformation(findLine(position)); + } + + @Override public final Line getLineInformation(int line) throws BadLocationException { int lines = fLines.size(); @@ -190,7 +255,7 @@ public final Line getLineInformation(int line) throws BadLocationException { return (l.delimiter != null ? new Line(l.offset, l.length - l.delimiter.length()) : l); } - // @Override + @Override public final int getLineOffset(int line) throws BadLocationException { int lines = fLines.size(); @@ -211,7 +276,7 @@ public final int getLineOffset(int line) throws BadLocationException { return l.offset; } - // @Override + @Override public final int getNumberOfLines() { int lines = fLines.size(); @@ -221,26 +286,31 @@ public final int getNumberOfLines() { Line l = fLines.get(lines - 1); return (l.delimiter != null ? lines + 1 : lines); } + + @Override + public final int getNumberOfLines(int position, int length) throws BadLocationException { - /* - * @Override public final int getNumberOfLines(int position, int length) throws - * BadLocationException { - * - * if (position < 0 || position + length > fTextLength) throw new - * BadLocationException(); - * - * if (length == 0) // optimization return 1; - * - * return getNumberOfLines(getLineNumberOfOffset(position), position, length); } - */ + if (position < 0 || position + length > fTextLength) + throw new BadLocationException(); - /* - * @Override 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; } - */ + if (length == 0) // optimization + return 1; + + return getNumberOfLines(getLineNumberOfOffset(position), position, length); + } + + @Override + 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; + } public final String getLineDelimiter(int line) throws BadLocationException { int lines = fLines.size(); @@ -348,7 +418,12 @@ private int createLines(String text, int insertPosition, int offset) { return count; } - // @Override + @Override + public final void replace(int position, int length, String text) throws BadLocationException { + throw new UnsupportedOperationException(); + } + + @Override public final void set(String text) { fLines.clear(); if (text != null) { diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/commons/TextDocument.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/commons/TextDocument.java index 8e520dff0..49c795c9d 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/commons/TextDocument.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/commons/TextDocument.java @@ -11,6 +11,8 @@ package org.eclipse.lsp4xml.commons; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,11 +28,13 @@ */ public class TextDocument extends TextDocumentItem { + private static final Logger LOGGER = Logger.getLogger(TextDocument.class.getName()); + private final Object lock = new Object(); private static String DEFAULT_DELIMTER = System.lineSeparator(); - private final ListLineTracker lineTracker; + private ILineTracker lineTracker; private boolean incremental; @@ -41,44 +45,40 @@ public TextDocument(TextDocumentItem document) { } public TextDocument(String text, String uri) { - this.lineTracker = new ListLineTracker(); super.setUri(uri); - this.setText(text); + super.setText(text); } public void setIncremental(boolean incremental) { this.incremental = incremental; + // reset line tracker + lineTracker = null; + getLineTracker(); } public boolean isIncremental() { return incremental; } - @Override - public void setText(String text) { - super.setText(text); - lineTracker.set(text); - } - public Position positionAt(int position) throws BadLocationException { - ListLineTracker lineTracker = getLineTracker(); + ILineTracker lineTracker = getLineTracker(); return lineTracker.getPositionAt(position); } public int offsetAt(Position position) throws BadLocationException { - ListLineTracker lineTracker = getLineTracker(); + ILineTracker lineTracker = getLineTracker(); return lineTracker.getOffsetAt(position); } public String lineText(int lineNumber) throws BadLocationException { - ListLineTracker lineTracker = getLineTracker(); + ILineTracker lineTracker = getLineTracker(); Line line = lineTracker.getLineInformation(lineNumber); String text = super.getText(); return text.substring(line.offset, line.offset + line.length); } public String lineDelimiter(int lineNumber) throws BadLocationException { - ListLineTracker lineTracker = getLineTracker(); + ILineTracker lineTracker = getLineTracker(); String lineDelimiter = lineTracker.getLineDelimiter(lineNumber); if (lineDelimiter == null) { if (lineTracker.getNumberOfLines() > 0) { @@ -94,7 +94,7 @@ public String lineDelimiter(int lineNumber) throws BadLocationException { public Range getWordRangeAt(int textOffset, Pattern wordDefinition) { try { Position pos = positionAt(textOffset); - ListLineTracker lineTracker = getLineTracker(); + ILineTracker lineTracker = getLineTracker(); Line line = lineTracker.getLineInformation(pos.getLine()); String text = super.getText(); String lineText = text.substring(line.offset, textOffset); @@ -118,7 +118,19 @@ public Range getWordRangeAt(int textOffset, Pattern wordDefinition) { } } - private ListLineTracker getLineTracker() { + private ILineTracker getLineTracker() { + if (lineTracker == null) { + lineTracker = createLineTracker(); + } + return lineTracker; + } + + private synchronized ILineTracker createLineTracker() { + if (lineTracker != null) { + return lineTracker; + } + ILineTracker lineTracker = isIncremental() ? new TreeLineTracker(new ListLineTracker()) : new ListLineTracker(); + lineTracker.set(super.getText()); return lineTracker; } @@ -135,18 +147,15 @@ public void update(ListILineTracker
. 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:
+ * + * This class must be subclassed. + *
+ *+ * Performance: The query operations perform in O(log n) + * where n is the number of lines in the document. The modification + * operations roughly perform in O(l * log n) where n is the + * number of lines in the document and l is the sum of the number of + * removed, added or modified lines. + *
+ * + * @since 3.2 + */ +public class TreeLineTracker implements ILineTracker { + + /** The predefined delimiters of this tracker */ + public final static String[] DELIMITERS = { "\r", "\n", "\r\n" }; //$NON-NLS-3$ //$NON-NLS-1$ //$NON-NLS-2$ + /** A predefined delimiter information which is always reused as return value */ + private DelimiterInfo fDelimiterInfo = new DelimiterInfo(); + + /* + * 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,null
if this is the root node. */
+ Node parent;
+ /** The left subtree, possibly null
. */
+ Node left;
+ /** The right subtree, possibly null
. */
+ Node right;
+ /** The balance factor. */
+ byte balance;
+
+ @Override
+ 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 null
.
+ */
+ 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
+ */
+ public TreeLineTracker(ListLineTracker tracker) {
+ final Listoffset
is returned.
+ * + * This means that for offsets smaller than the length, the following holds: + *
+ *
+ * line.offset <= offset < line.offset + offset.length
.
+ *
+ * If offset
is the document length, then this is true:
+ *
+ * offset= line.offset + line.length
.
+ *
offset
+ * @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 offset
is returned. The last line is
+ * returned if offset
is equal to the document length.
+ *
+ * @param offset a document offset
+ * @return the line number starting at or containing offset
+ * @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 node
.
+ *
+ * @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 node
.
+ *
+ * @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 child
, null
to
+ * replace the root node
+ * @param child the new child of parent
, may be
+ * null
+ * @param isLeftChild true
if child
shall become
+ * parent
's left child, false
if it
+ * shall become parent
'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 parent
, whose structural position is
+ * replaced by node
.
+ *
+ * @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 parent
, whose structural position is
+ * replaced by node
.
+ *
+ * @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 node
, then
+ * left around parent
.
+ *
+ * @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 node
, then
+ * right around parent
.
+ *
+ * @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 node
.
+ *
+ * @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);
+ }
+ }
+
+ @Override
+ 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, < firstLineDelta
+ * @param firstLineDelta the number of characters from the replacement offset to
+ * the end of node
> length
+ */
+ 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, >= firstLineDelta
+ * @param firstLineDelta the number of characters removed from the replacement
+ * offset to the end of node
, <=
+ * length
+ */
+ 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 delta
. 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 delta
, also adjusting the parent
+ * chain of node
. 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
+ * from.parent
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
+ * from.parent
to to
(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 delete
directly, but call
+ * update_length(node, -node.length)
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 true
if the deletion happened on
+ * node
's left subtree, false
if
+ * it occurred on node
'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 true
if the re-balancement leaves the height at
+ * node.parent
constant, false
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 true
if the re-balancement leaves the height at
+ * node.parent
constant, false
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, null
if node is the last node.
+ *
+ * @param node a node
+ * @return the successor of node
, null
if there is
+ * none
+ */
+ private Node successor(Node node) {
+ if (node.right != null)
+ return successorDown(node.right);
+
+ return successorUp(node);
+ }
+
+ /**
+ * Searches the successor of node
in its parent chain.
+ *
+ * @param node a node
+ * @return the first node in node
's parent chain that is reached
+ * from its left subtree, null
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 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 null
+ */
+ protected DelimiterInfo nextDelimiterInfo(String text, int offset) {
+ char ch;
+ int length = text.length();
+ for (int i = offset; i < length; i++) {
+
+ ch = text.charAt(i);
+ if (ch == '\r') {
+
+ if (i + 1 < length) {
+ if (text.charAt(i + 1) == '\n') {
+ fDelimiterInfo.delimiter = DELIMITERS[2];
+ fDelimiterInfo.delimiterIndex = i;
+ fDelimiterInfo.delimiterLength = 2;
+ return fDelimiterInfo;
+ }
+ }
+
+ fDelimiterInfo.delimiter = DELIMITERS[0];
+ fDelimiterInfo.delimiterIndex = i;
+ fDelimiterInfo.delimiterLength = 1;
+ return fDelimiterInfo;
+
+ } else if (ch == '\n') {
+
+ fDelimiterInfo.delimiter = DELIMITERS[1];
+ fDelimiterInfo.delimiterIndex = i;
+ fDelimiterInfo.delimiterLength = 1;
+ return fDelimiterInfo;
+ }
+ }
+
+ return null;
+
+ }
+
+ @Override
+ public final String getLineDelimiter(int line) throws BadLocationException {
+ Node node = nodeByLine(line);
+ return node.delimiter == NO_DELIM ? null : node.delimiter;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ public final int getLineOffset(int line) throws BadLocationException {
+ return offsetByLine(line);
+ }
+
+ @Override
+ public final int getLineLength(int line) throws BadLocationException {
+ Node node = nodeByLine(line);
+ return node.length;
+ }
+
+ @Override
+ public final int getLineNumberOfOffset(int offset) throws BadLocationException {
+ return lineByOffset(offset);
+ }
+
+ public final Position getPositionAt(int offset) throws BadLocationException {
+ Line l = getLineInformationOfOffset(offset);
+ int lineNumber = getLineNumberOfOffset(offset);
+ int character = offset - l.offset;
+ return new Position(lineNumber, character);
+ }
+
+ @Override
+ public int getOffsetAt(Position position) throws BadLocationException {
+ int line = position.getLine();
+ Line l = getLineInformation(line);
+
+ if (line < 0/* || line > lines */)
+ throw new BadLocationException("The line value, {" + line + "}, is out of bounds.");
+
+ int lineOffset = l.offset;
+ int lineLength = l.delimiter != null ? l.length - l.delimiter.length() : l.length;
+
+// int lineOffset = -1;
+// int lineLength = -1;
+// if (lines == 0) {
+// lineOffset = 0;
+// lineLength = 0;
+// } else {
+// if (line == lines) {
+// Line l = fLines.get(line - 1);
+// lineOffset = l.offset + l.length;
+// lineLength = 0;
+// } else {
+// Line l = fLines.get(line);
+// lineOffset = l.offset;
+// lineLength = l.delimiter != null ? l.length - l.delimiter.length() : l.length;
+// }
+// }
+ int character = position.getCharacter();
+ int offset = lineOffset + character;
+ int endLineOffset = lineOffset + lineLength;
+ if (offset > endLineOffset)
+ throw new BadLocationException(
+ "The character value, {" + character + "} of the line" + line + "}, is out of bounds.");
+ return offset;
+
+ }
+
+ @Override
+ public final Line 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 Line(lineOffset, node.pureLength());
+ }
+
+ @Override
+ public final Line 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 Line(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 Line(offset + last.length, 0);
+ }
+ throw x;
+ }
+ }
+
+ @Override
+ public final void set(String text) {
+ fRoot = new Node(0, NO_DELIM);
+ try {
+ replace(0, 0, text);
+ } catch (BadLocationException x) {
+ throw new InternalError();
+ }
+ }
+
+ @Override
+ 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$
+
+ Listnull
+ * @return the depth of the given tree, 0 if it is null
+ */
+ 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 node
.
+ * 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 node
+ */
+ 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 node
and continuing until last
.
+ *
+ * @param node the first Node
to check, may be null
+ * @param offLen an array of length 2, with offLen[0]
the expected
+ * offset of node
and offLen[1]
the
+ * expected line of node
+ * @param last the last Node
to check, may be null
+ * @return an int[]
of length 2, with the first element being the
+ * character length of node
's subtree, and the second
+ * element the number of lines in node
'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);
+ }
+}
\ No newline at end of file
diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/logs/LogHelper.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/logs/LogHelper.java
index 8bc085ce1..0ed9e0b8b 100644
--- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/logs/LogHelper.java
+++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/logs/LogHelper.java
@@ -37,9 +37,9 @@ public static void initializeRootLogger(LanguageClient newLanguageClient, LogsSe
Logger logger = Logger.getLogger("");
unregisterAllHandlers(logger.getHandlers());
- logger.setLevel(Level.INFO);
+ logger.setLevel(getLogLevel());
logger.setUseParentHandlers(false);// Stops output to console
-
+
// Configure logging LSP client handler
if (settings.getClient()) {
try {
@@ -58,14 +58,37 @@ public static void initializeRootLogger(LanguageClient newLanguageClient, LogsSe
logger.addHandler(fh);
} catch (SecurityException | IOException e) {
- logger.log(Level.WARNING, "Error at creation of FileHandler for logging");
+ logger.warning("Error at creation of FileHandler for logging");
}
} else {
- logger.log(Level.INFO, "Log file could not be created, path not provided");
+ logger.info("Log file could not be created, path not provided");
}
}
+ private static Level getLogLevel() {
+ String logLevel = System.getProperty("log.level", "info").toLowerCase();
+ switch (logLevel) {
+ case "info":
+ return Level.INFO;
+ case "off":
+ return Level.OFF;
+ case "all":
+ case "debug":
+ case "fine":
+ case "finer":
+ case "finest":
+ return Level.FINEST;
+ case "warn":
+ case "warning":
+ return Level.WARNING;
+ case "error":
+ case "fatal":
+ return Level.SEVERE;
+ }
+ return Level.INFO;
+ }
+
private static void createDirectoryPath(String path) {
Path parentPath = Paths.get(path).normalize().getParent();
if (parentPath != null) {
diff --git a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/commons/TextDocumentTest.java b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/commons/TextDocumentTest.java
index 429e0bce1..c6937ce31 100644
--- a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/commons/TextDocumentTest.java
+++ b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/commons/TextDocumentTest.java
@@ -20,6 +20,8 @@
*/
public class TextDocumentTest {
+ // Test with non incremental (with ListLineTracker)
+
@Test
public void testEmptyDocument() throws BadLocationException {
TextDocument document = new TextDocument("", "");
@@ -132,4 +134,122 @@ public void testOffsetAt() throws BadLocationException {
Assert.assertNotNull(ex);
}
+ // Test with incremental (with TreeLineTracker)
+
+ @Test
+ public void testEmptyDocumentInc() throws BadLocationException {
+ TextDocument document = new TextDocument("", "");
+ document.setIncremental(true);
+
+ Position position = document.positionAt(0);
+ Assert.assertEquals(0, position.getLine());
+ Assert.assertEquals(0, position.getCharacter());
+
+ position = new Position(0, 0);
+ int offset = document.offsetAt(position);
+ Assert.assertEquals(0, offset);
+
+ }
+
+ @Test
+ public void testPositionAtInc() throws BadLocationException {
+ TextDocument document = new TextDocument("abcd\nefgh", "");
+ document.setIncremental(true);
+
+ Position position = document.positionAt(0);
+ Assert.assertEquals(0, position.getLine());
+ Assert.assertEquals(0, position.getCharacter());
+
+ position = document.positionAt(4);
+ Assert.assertEquals(0, position.getLine());
+ Assert.assertEquals(4, position.getCharacter());
+
+ position = document.positionAt(5);
+ Assert.assertEquals(1, position.getLine());
+ Assert.assertEquals(0, position.getCharacter());
+
+ position = document.positionAt(9);
+ Assert.assertEquals(1, position.getLine());
+ Assert.assertEquals(4, position.getCharacter());
+
+ BadLocationException ex = null;
+ try {
+ position = document.positionAt(10);
+ } catch (BadLocationException e) {
+ ex = e;
+ }
+ Assert.assertNotNull(ex);
+ }
+
+ @Test
+ public void testPositionAtEndLineInc() throws BadLocationException {
+ TextDocument document = new TextDocument("abcd\n", "");
+ document.setIncremental(true);
+
+ Position position = document.positionAt(4);
+ Assert.assertEquals(0, position.getLine());
+ Assert.assertEquals(4, position.getCharacter());
+
+ position = document.positionAt(5);
+ Assert.assertEquals(1, position.getLine());
+ Assert.assertEquals(0, position.getCharacter());
+
+ BadLocationException ex = null;
+ try {
+ position = document.positionAt(6);
+ } catch (BadLocationException e) {
+ ex = e;
+ }
+ Assert.assertNotNull(ex);
+
+ document = new TextDocument("abcd\nefgh\n", "");
+
+ position = document.positionAt(9);
+ Assert.assertEquals(1, position.getLine());
+ Assert.assertEquals(4, position.getCharacter());
+
+ position = document.positionAt(10);
+ Assert.assertEquals(2, position.getLine());
+ Assert.assertEquals(0, position.getCharacter());
+
+ ex = null;
+ try {
+ position = document.positionAt(11);
+ } catch (BadLocationException e) {
+ ex = e;
+ }
+ Assert.assertNotNull(ex);
+ }
+
+ @Test
+ public void testOffsetAtInc() throws BadLocationException {
+ TextDocument document = new TextDocument("abcd\nefgh", "");
+ document.setIncremental(true);
+
+ Position position = new Position(0, 0);
+ int offset = document.offsetAt(position);
+ Assert.assertEquals(0, offset);
+
+ position = new Position(0, 4);
+ offset = document.offsetAt(position);
+ Assert.assertEquals(4, offset);
+
+ position = new Position(1, 0);
+ offset = document.offsetAt(position);
+ Assert.assertEquals(5, offset);
+
+ position = new Position(1, 4);
+ offset = document.offsetAt(position);
+ Assert.assertEquals(9, offset);
+
+ BadLocationException ex = null;
+ try {
+ position = new Position(1, 5);
+ document.offsetAt(position);
+ } catch (BadLocationException e) {
+ ex = e;
+ }
+ Assert.assertNotNull(ex);
+ }
+
}
diff --git a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/performance/TextDocumentUpdatePerformance.java b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/performance/TextDocumentUpdatePerformance.java
new file mode 100644
index 000000000..2673859b0
--- /dev/null
+++ b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/performance/TextDocumentUpdatePerformance.java
@@ -0,0 +1,51 @@
+/*******************************************************************************
+* Copyright (c) 2019 Red Hat Inc. and others.
+* All rights reserved. This program and the accompanying materials
+* which accompanies this distribution, and is available at
+* http://www.eclipse.org/legal/epl-v20.html
+*
+* Contributors:
+* Red Hat Inc. - initial API and implementation
+*******************************************************************************/
+package org.eclipse.lsp4xml.performance;
+
+import static org.eclipse.lsp4xml.utils.IOUtils.convertStreamToString;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.lsp4j.Position;
+import org.eclipse.lsp4j.Range;
+import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
+import org.eclipse.lsp4xml.commons.TextDocument;
+
+/**
+ * This utility class is used to check the performance of
+ * {@link TextDocument#update(List)}, updating the large nasa.xml file
+ *
+ * @author Angelo ZERR
+ *
+ */
+public class TextDocumentUpdatePerformance {
+
+ public static void main(String[] args) {
+ InputStream in = TextDocumentUpdatePerformance.class.getResourceAsStream("/xml/nasa.xml");
+ String text = convertStreamToString(in);
+ TextDocument document = new TextDocument(text, "nasa.xml");
+ document.setIncremental(true);
+ // Continuously parses the large nasa.xml file with the DOM parser.
+ while (true) {
+ long start = System.currentTimeMillis();
+ // Insert a space
+ List