com.intellij.util.text.ImmutableText Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of symbol-processing-aa Show documentation
Show all versions of symbol-processing-aa Show documentation
KSP implementation on Kotlin Analysis API
/*
* Javolution - Java(tm) Solution for Real-Time and Embedded Systems
* Copyright (c) 2012, Javolution (http://javolution.org/)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.intellij.util.text;
import com.intellij.openapi.util.text.CharSequenceWithStringHash;
import com.intellij.openapi.util.text.Strings;
import org.jetbrains.annotations.NotNull;
/**
* A pruned and optimized version of javolution.text.Text
*
* This class represents an immutable character sequence with
* fast {@link #concat concatenation}, {@link #insert insertion} and
* {@link #delete deletion} capabilities (O[Log(n)]) instead of
* O[n] for StringBuffer/StringBuilder).
*
* Implementation Note: To avoid expensive copy operations ,
* {@link ImmutableText} instances are broken down into smaller immutable
* sequences, they form a minimal-depth binary tree.
* The tree is maintained balanced automatically through tree rotations.
* Insertion/deletions are performed in {@code O[Log(n)]}
* instead of {@code O[n]} for
* {@code StringBuffer/StringBuilder}.
*
* @author Jean-Marie Dautelle
* @author Wilfried Middleton
* @version 5.3, January 10, 2007
*/
final class ImmutableText extends ImmutableCharSequence implements CharArrayExternalizable, CharSequenceWithStringHash {
/**
* Holds the default size for primitive blocks of characters.
*/
private static final int BLOCK_SIZE = 1 << 6;
/**
* Holds the mask used to ensure a block boundary cesures.
*/
private static final int BLOCK_MASK = -BLOCK_SIZE;
// visible for tests
// Here (String | CompositeNode | ByteArrayCharSequence) is stored
final CharSequence myNode;
private ImmutableText(CharSequence node) {
myNode = node;
}
/**
* Returns the text representing the specified object.
*
* @param obj the object to represent as text.
* @return the textual representation of the specified object.
*/
static ImmutableText valueOf(@NotNull Object obj) {
if (obj instanceof ImmutableText) return (ImmutableText)obj;
if (obj instanceof CharSequence) return valueOf((CharSequence)obj);
return valueOf(String.valueOf(obj));
}
private static ImmutableText valueOf(@NotNull CharSequence str) {
if (str instanceof ByteArrayCharSequence) {
return new ImmutableText(str);
}
if (str.length() == 0) {
return EMPTY;
}
return new ImmutableText(str.toString());
}
/**
* When first loaded, ImmutableText contents are stored as a single large array. This saves memory but isn't
* modification-friendly as it disallows slightly changed texts to retain most of the internal structure of the
* original document. Whoever retains old non-chunked version will use more memory than really needed.
*
* @return a copy of the myNode better prepared for small modifications to fully enable structure-sharing capabilities
*/
private CharSequence ensureChunked() {
if (length() > BLOCK_SIZE && !(myNode instanceof CompositeNode)) {
return nodeOf(myNode, 0, length());
}
return myNode;
}
private static CharSequence nodeOf(@NotNull CharSequence node, int offset, int length) {
if (length <= BLOCK_SIZE) {
// Use toString to avoid referencing the original byte[] array in case if node is ByteArrayCharSequence
return node.subSequence(offset, offset + length).toString();
}
// Splits on a block boundary.
int half = ((length + BLOCK_SIZE) >> 1) & BLOCK_MASK;
return new CompositeNode(nodeOf(node, offset, half), nodeOf(node, offset + half, length - half));
}
private static final ImmutableText EMPTY = new ImmutableText("");
/**
* Returns the length of this text.
*
* @return the number of characters (16-bits Unicode) composing this text.
*/
@Override
public int length() {
return myNode.length();
}
/**
* Concatenates the specified text to the end of this text.
* This method is very fast (faster even than
* {@code StringBuffer.append(String)}) and still returns
* a text instance with an internal binary tree of minimal depth!
*
* @param that the text that is concatenated.
* @return {@code this + that}
*/
private ImmutableText concat(ImmutableText that) {
return that.length() == 0 ? this : length() == 0 ? that : new ImmutableText(concatNodes(ensureChunked(), that.ensureChunked()));
}
@Override
public ImmutableText concat(@NotNull CharSequence sequence) {
return concat(valueOf(sequence));
}
/**
* Returns a portion of this text.
*
* @param start the index of the first character inclusive.
* @return the sub-text starting at the specified position.
* @throws IndexOutOfBoundsException if {@code (start < 0) ||
* (start > this.length())}
*/
private ImmutableText subtext(int start) {
return subtext(start, length());
}
@Override
public ImmutableCharSequence replace(int start, int end, @NotNull CharSequence seq) {
if (start == end) return insert(start, seq);
if (seq.length() == 0) return delete(start, end);
if (start > end) {
throw new IndexOutOfBoundsException();
}
return subtext(0, start).concat(valueOf(seq)).concat(subtext(end));
}
@Override
public ImmutableText insert(int index, @NotNull CharSequence seq) {
if (seq.length() == 0) return this;
return subtext(0, index).concat(valueOf(seq)).concat(subtext(index));
}
/**
* Returns the text without the characters between the specified indexes.
*
* @param start the beginning index, inclusive.
* @param end the ending index, exclusive.
* @return {@code subtext(0, start).concat(subtext(end))}
* @throws IndexOutOfBoundsException if {@code (start < 0) || (end < 0) ||
* (start > end) || (end > this.length()}
*/
@Override
public ImmutableText delete(int start, int end) {
if (start == end) return this;
if (start > end) {
throw new IndexOutOfBoundsException();
}
return subtext(0, start).concat(subtext(end));
}
@Override
public CharSequence subSequence(final int start, final int end) {
if (start == 0 && end == length()) return this;
return new CharSequenceSubSequence(this, start, end);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ImmutableText)) {
return false;
}
return CharArrayUtil.regionMatches(this, 0, (ImmutableText)obj);
}
private transient int hash;
/**
* Returns the hash code for this text.
*
* @return the hash code value.
*/
@Override
public int hashCode() {
int h = hash;
if (h == 0) {
hash = h = Strings.stringHashCode(this, 0, length());
}
return h;
}
@Override
public char charAt(int index) {
InnerLeaf leaf = myLastLeaf;
if (leaf == null || index < leaf.start || index >= leaf.end) {
myLastLeaf = leaf = findLeaf(index);
}
return leaf.leafNode.charAt(index - leaf.start);
}
private InnerLeaf myLastLeaf;
private InnerLeaf findLeaf(int index) {
if (index < 0) throw outOfRange(index);
CharSequence node = myNode;
int nodeLength = node.length();
int offset = 0;
while (true) {
if (index >= nodeLength) {
throw outOfRange(index);
}
if (!(node instanceof CompositeNode)) {
return new InnerLeaf(node, offset, offset + nodeLength);
}
CompositeNode composite = (CompositeNode)node;
int headLength = composite.head.length();
if (index < headLength) {
node = composite.head;
nodeLength = headLength;
}
else {
offset += headLength;
index -= headLength;
node = composite.tail;
nodeLength -= headLength;
}
}
}
private IndexOutOfBoundsException outOfRange(int index) {
return new IndexOutOfBoundsException("Index out of range: " + index+"; length: "+length());
}
private static final class InnerLeaf {
final CharSequence leafNode;
final int start;
final int end;
private InnerLeaf(@NotNull CharSequence leafNode, int start, int end) {
this.leafNode = leafNode;
this.start = start;
this.end = end;
}
}
/**
* Returns a portion of this text.
*
* @param start the index of the first character inclusive.
* @param end the index of the last character exclusive.
* @return the sub-text starting at the specified start position and
* ending just before the specified end position.
* @throws IndexOutOfBoundsException if {@code (start < 0) || (end < 0) ||
* (start > end) || (end > this.length())}
*/
@Override
public ImmutableText subtext(int start, int end) {
if (start < 0 || start > end || end > length()) {
throw new IndexOutOfBoundsException();
}
if (start == 0 && end == length()) {
return this;
}
if (start == end) {
return EMPTY;
}
return new ImmutableText(myNode.subSequence(start, end));
}
/**
* Copies the characters from this text into the destination
* character array.
*
* @param start the index of the first character to copy.
* @param end the index after the last character to copy.
* @param dest the destination array.
* @param destPos the start offset in the destination array.
* @throws IndexOutOfBoundsException if {@code (start < 0) || (end < 0) ||
* (start > end) || (end > this.length())}
*/
@Override
public void getChars(int start, int end, char @NotNull [] dest, int destPos) {
getChars(myNode, start, end, dest, destPos);
}
private static void getChars(CharSequence cs, int start, int end, char @NotNull [] dest, int destPos) {
if (cs instanceof String) {
((String)cs).getChars(start, end, dest, destPos);
}
else if (cs instanceof ByteArrayCharSequence) {
((ByteArrayCharSequence)cs).getChars(start, end, dest, destPos);
}
else {
((CompositeNode)cs).getChars(start, end, dest, destPos);
}
}
/**
* Returns the {@code String} representation of this text.
*
* @return the {@code java.lang.String} for this text.
*/
@Override
@NotNull
public String toString() {
return myNode.toString();
}
@NotNull
private static CharSequence concatNodes(@NotNull CharSequence node1, @NotNull CharSequence node2) {
// All Text instances are maintained balanced:
// (head < tail * 2) & (tail < head * 2)
final int length = node1.length() + node2.length();
if (length <= BLOCK_SIZE) { // Merges to primitive.
// module is still targeted to Java 8, so plus-concatenation is compiled via StringBuilder
// here concat() looks preferred
//noinspection CallToStringConcatCanBeReplacedByOperator
return node1.toString().concat(node2.toString());
}
// Returns a composite.
CharSequence head = node1;
CharSequence tail = node2;
if (shouldRebalance(head, tail)) {
// head too small, returns (head + tail/2) + (tail/2)
do {
if (((CompositeNode)tail).head.length() > ((CompositeNode)tail).tail.length()) {
// Rotates to concatenate with smaller part.
tail = ((CompositeNode)tail).rightRotation();
}
head = concatNodes(head, ((CompositeNode)tail).head);
tail = ((CompositeNode)tail).tail;
}
while (shouldRebalance(head, tail));
}
else if (shouldRebalance(tail, head)) {
// tail too small, returns (head/2) + (head/2 concat tail)
do {
if (((CompositeNode)head).tail.length() > ((CompositeNode)head).head.length()) {
// Rotates to concatenate with smaller part.
head = ((CompositeNode)head).leftRotation();
}
tail = concatNodes(((CompositeNode)head).tail, tail);
head = ((CompositeNode)head).head;
}
while (shouldRebalance(tail, head));
}
return new CompositeNode(head, tail);
}
private static boolean shouldRebalance(CharSequence shorter, CharSequence longer) {
return (shorter.length() << 1) < longer.length() && longer instanceof CompositeNode;
}
static final class CompositeNode implements CharSequence {
final int count;
final CharSequence head;
final CharSequence tail;
CompositeNode(CharSequence head, CharSequence tail) {
count = head.length() + tail.length();
this.head = head;
this.tail = tail;
}
@Override
public int length() {
return count;
}
@Override
public char charAt(int index) {
int headLength = head.length();
return index < headLength ? head.charAt(index) : tail.charAt(index - headLength);
}
CompositeNode rightRotation() {
// See: http://en.wikipedia.org/wiki/Tree_rotation
CharSequence P = this.head;
if (!(P instanceof CompositeNode)) {
return this; // Head not a composite, cannot rotate.
}
CharSequence A = ((CompositeNode)P).head;
CharSequence B = ((CompositeNode)P).tail;
//noinspection UnnecessaryLocalVariable
CharSequence C = this.tail;
return new CompositeNode(A, new CompositeNode(B, C));
}
CompositeNode leftRotation() {
// See: http://en.wikipedia.org/wiki/Tree_rotation
CharSequence Q = this.tail;
if (!(Q instanceof CompositeNode)) {
return this; // Tail not a composite, cannot rotate.
}
CharSequence B = ((CompositeNode)Q).head;
CharSequence C = ((CompositeNode)Q).tail;
//noinspection UnnecessaryLocalVariable
CharSequence A = this.head;
return new CompositeNode(new CompositeNode(A, B), C);
}
void getChars(int start, int end, char @NotNull [] dest, int destPos) {
final int cesure = head.length();
if (end <= cesure) {
ImmutableText.getChars(head, start, end, dest, destPos);
}
else if (start >= cesure) {
ImmutableText.getChars(tail, start - cesure, end - cesure, dest, destPos);
}
else { // Overlaps head and tail.
ImmutableText.getChars(head, start, cesure, dest, destPos);
ImmutableText.getChars(tail, 0, end - cesure, dest, destPos + cesure - start);
}
}
@Override
public CharSequence subSequence(int start, int end) {
final int cesure = head.length();
if (end <= cesure) {
return head.subSequence(start, end);
}
if (start >= cesure) {
return tail.subSequence(start - cesure, end - cesure);
}
if (start == 0 && end == count) {
return this;
}
// Overlaps head and tail.
if (end - start < BLOCK_SIZE) {
char[] data = new char[end - start];
ImmutableText.getChars(head, start, cesure, data, 0);
ImmutableText.getChars(tail, 0, end - cesure, data, cesure - start);
return new String(data);
}
return concatNodes(head.subSequence(start, cesure), tail.subSequence(0, end - cesure));
}
@NotNull
@Override
public String toString() {
int len = length();
char[] data = new char[len];
getChars(0, len, data, 0);
return new String(data);
}
}
}