//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
//
// This file contain implementations details that are subject to change without notice.
// Use at your own risk.
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
namespace Microsoft.VisualStudio.Text.Implementation
{
///
/// An immutable variation on the StringBuilder class.
///
internal abstract class StringRebuilder
{
public readonly static StringRebuilder Empty = new StringRebuilderForString();
#if DEBUG
protected static int _totalCharactersScanned = 0;
public static int TotalCharactersScanned { get { return _totalCharactersScanned; } }
protected static int _totalCharactersReturned = 0;
public static int TotalCharactersReturned { get { return _totalCharactersReturned; } }
protected static int _totalCharactersCopied = 0;
public static int TotalCharactersCopied { get { return _totalCharactersCopied; } }
#endif
public static StringRebuilder Create(string text)
{
if (text == null)
throw new ArgumentNullException("text");
#if DEBUG
Interlocked.Add(ref _totalCharactersScanned, text.Length);
#endif
return (text.Length == 0)
? StringRebuilder.Empty
: StringRebuilderForString.Create(text, text.Length, LineBreakManager.CreateLineBreaks(text));
}
public static StringRebuilder Create(ITextImage image)
{
if (image == null)
throw new ArgumentNullException(nameof(image));
var cti = image as CachingTextImage;
if (cti != null)
return cti.Builder;
// This shouldn't happen but as a fallback, create a new string rebuilder from the text of the provided image.
return StringRebuilder.Create(image.GetText(0, image.Length));
}
///
/// Consolidate two string rebuilders, taking advantage of the fact that they have already extracted the line breaks.
///
public static StringRebuilder Consolidate(StringRebuilder left, StringRebuilder right)
{
Debug.Assert(left.Length > 0);
Debug.Assert(right.Length > 0);
int length = left.Length + right.Length;
char[] result = new char[length];
left.CopyTo(0, result, 0, left.Length);
right.CopyTo(0, result, left.Length, right.Length);
ILineBreaks lineBreaks;
if ((left.LineBreakCount == 0) && (right.LineBreakCount == 0))
{
lineBreaks = LineBreakManager.Empty;
//_lineBreakSpan defaults to 0, 0 which is what we want
}
else
{
ILineBreaksEditor breaks = LineBreakManager.CreateLineBreakEditor(length, left.LineBreakCount + right.LineBreakCount);
int offset = 0;
if ((result[left.Length] == '\n') && (result[left.Length - 1] == '\r'))
{
//We have a \r\n spanning the seam ... add that as a special linebreak later.
offset = 1;
}
int leftLines = left.LineBreakCount - offset;
for (int i = 0; (i < leftLines); ++i)
{
Span extent;
int lineBreakLength;
left.GetLineFromLineNumber(i, out extent, out lineBreakLength);
breaks.Add(extent.End, lineBreakLength);
}
if (offset == 1)
{
breaks.Add(left.Length - 1, 2);
}
for (int i = offset; (i < right.LineBreakCount); ++i)
{
Span extent;
int lineBreakLength;
right.GetLineFromLineNumber(i, out extent, out lineBreakLength);
breaks.Add(extent.End + left.Length, lineBreakLength);
}
lineBreaks = breaks;
}
return StringRebuilderForChars.Create(result, length, lineBreaks);
}
protected StringRebuilder(int length, int lineBreakCount, char first, char last)
{
this.Length = length;
this.LineBreakCount = lineBreakCount;
this.FirstCharacter = first;
this.LastCharacter = last;
}
///
/// Number of characters in this .
///
public readonly int Length;
///
/// Number of line breaks in this .
///
/// Line breaks consist of any of '\r', '\n', 0x85,
/// or a "\r\n" pair (which is treated as a single line break).
public int LineBreakCount;
public virtual int Depth => 0;
///
/// The first character of the StringRebuilder. \0 for a zero length StringRebuilder.
///
public readonly char FirstCharacter;
///
/// The last character of the StringRebuilder. \0 for a zero length StringRebuilder.
///
public readonly char LastCharacter;
#region Abstract methods
///
/// Get the zero-based line number that contains .
///
/// Position of the character for which to get the line number.
/// Number of the line that contains .
///
/// Lines are bounded by line breaks and the start and end of this .
///
/// is less than zero or greater than .
public abstract int GetLineNumberFromPosition(int position);
///
/// Get the TextImageLine associated with a zero-based line number.
///
/// Line number for which to get the TextImageLine.
///
/// The last "line" in the StringRebuilder has an implicit line break length of zero.
///
/// is less than zero or greater than .
public abstract void GetLineFromLineNumber(int lineNumber, out Span extent, out int lineBreakLength);
///
/// Get the "leaf" node of the string rebuilder that contains position.
///
/// position for which to get the leaf.
/// number of characters to the left of the leaf.
/// leaf node from the string rebuilder.
public abstract StringRebuilder GetLeaf(int position, out int offset);
///
/// Character at the given index.
///
/// Index to get the character for.
/// Character at position .
/// is less than zero or greater than or equal to .
public abstract char this[int index] { get; }
///
/// Copy a range of text to a destination character array.
///
///
/// The starting index to copy from.
///
///
/// The destination array.
///
///
/// The index in the destination of the first position to be copied to.
///
///
/// The number of characters to copy.
///
/// is less than zero or greater than .
/// is less than zero or + is greater than .
/// is less than zero or + is greater than the length of .
/// is null.
public abstract void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count);
///
/// Write a substring of the contents of this to a TextWriter.
///
/// TextWriter to use.
/// Span to write.
/// is null.
/// .End is greater than .
public abstract void Write(TextWriter writer, Span span);
///
/// Create a new StringRebuilder that corresponds to a substring of this .
///
/// span that defines the desired substring.
/// A new StringRebuilder containing the substring.
///
/// this is not modified.
/// This operation can be performed simultaneously on multiple threads.
///
/// .End is greater than .
public abstract StringRebuilder GetSubText(Span span);
///
/// Get the string that contains all of the characters in the specified span.
///
/// Span for which to get the text.
///
///
/// this can contain millions of characters. Be careful what you
/// ask for: you might get it.
/// This operation can be performed simultaneously on multiple threads.
///
/// .End is greater than .
public abstract string GetText(Span span);
public abstract StringRebuilder Child(bool rightSide);
#endregion
///
/// Convert a range of text to a character array.
///
///
/// The starting index of the range of text.
///
///
/// The length of the text.
///
/// is less than zero or greater than .
/// is less than zero or + is greater than .
public char[] ToCharArray(int startIndex, int length)
{
if (startIndex < 0)
throw new ArgumentOutOfRangeException("startIndex");
if ((length < 0) || (startIndex + length > this.Length) || (startIndex + length < 0))
throw new ArgumentOutOfRangeException("length");
char[] copy = new char[length];
this.CopyTo(startIndex, copy, 0, length);
return copy;
}
///
/// Create a new StringRebuilder equivalent to appending text into this .
///
/// Text to append.
/// A new StringRebuilder containing the insertion.
///
/// this is not modified.
/// This operation can be performed simultaneously on multiple threads.
///
/// is null.
public StringRebuilder Append(string text)
{
return this.Insert(this.Length, text);
}
///
/// Create a new StringRebuilder equivalent to appending text into this .
///
/// Text to append.
/// A new StringRebuilder containing the insertion.
///
/// this is not modified.
/// This operation can be performed simultaneously on multiple threads.
///
/// is null.
public StringRebuilder Append(StringRebuilder text)
{
return this.Insert(this.Length, text);
}
///
/// Create a new StringRebuilder equivalent to inserting text into this .
///
/// Position at which to insert.
/// Text to insert.
/// A new StringRebuilder containing the insertion.
///
/// this is not modified.
/// This operation can be performed simultaneously on multiple threads.
///
/// is less than zero or greater than .
/// is null.
public StringRebuilder Insert(int position, string text)
{
return this.Insert(position, StringRebuilder.Create(text));
}
///
/// Create a new StringRebuilder equivalent to inserting text into this .
///
/// Position at which to insert.
/// Text to insert.
/// A new StringRebuilder containing the insertion.
///
/// this is not modified.
/// This operation can be performed simultaneously on multiple threads.
///
/// is less than zero or greater than .
/// is null.
public StringRebuilder Insert(int position, StringRebuilder text)
{
if ((position < 0) || (position > this.Length))
throw new ArgumentOutOfRangeException("position");
if (text == null)
throw new ArgumentNullException("text");
return this.Assemble(Span.FromBounds(0, position), text, Span.FromBounds(position, this.Length));
}
///
/// Create a new StringRebuilder equivalent to deleting text from this .
///
/// Span of text to delete.
/// A new StringRebuilder containing the deletion.
///
/// this is not modified.
/// This operation can be performed simultaneously on multiple threads.
///
/// .End is greater than .
public StringRebuilder Delete(Span span)
{
if (span.End > this.Length)
throw new ArgumentOutOfRangeException("span");
return this.Assemble(Span.FromBounds(0, span.Start), Span.FromBounds(span.End, this.Length));
}
///
/// Create a new StringRebuilder equivalent to replacing a contiguous span of characters
/// with different text.
///
///
/// Span of text in this to replace.
///
///
/// The new text to replace the old.
///
///
/// A new string rebuilder containing the replacement.
///
///
/// this is not modified.
/// This operation can be performed simultaneously on multiple threads.
///
/// .End is greater than .
/// is null.
public StringRebuilder Replace(Span span, string text)
{
return this.Replace(span, StringRebuilder.Create(text));
}
///
/// Create a new StringRebuilder equivalent to replacing a contiguous span of characters
/// with different text.
///
///
/// Span of text in this to replace.
///
///
/// The new text to replace the old.
///
///
/// A new string rebuilder containing the replacement.
///
///
/// this is not modified.
/// This operation can be performed simultaneously on multiple threads.
///
/// .End is greater than .
/// is null.
public StringRebuilder Replace(Span span, StringRebuilder text)
{
if (span.End > this.Length)
throw new ArgumentOutOfRangeException("span");
if (text == null)
throw new ArgumentNullException("text");
return this.Assemble(Span.FromBounds(0, span.Start), text, Span.FromBounds(span.End, this.Length));
}
#region Private
private StringRebuilder Assemble(Span left, Span right)
{
if (left.Length == 0)
return this.GetSubText(right);
else if (right.Length == 0)
return this.GetSubText(left);
else if (left.Length + right.Length == this.Length)
return this;
else
return BinaryStringRebuilder.Create(this.GetSubText(left), this.GetSubText(right));
}
private StringRebuilder Assemble(Span left, StringRebuilder text, Span right)
{
if (text.Length == 0)
return Assemble(left, right);
else if (left.Length == 0)
return (right.Length == 0) ? text : BinaryStringRebuilder.Create(text, this.GetSubText(right));
else if (right.Length == 0)
return BinaryStringRebuilder.Create(this.GetSubText(left), text);
else if (left.Length < right.Length)
return BinaryStringRebuilder.Create(BinaryStringRebuilder.Create(this.GetSubText(left), text),
this.GetSubText(right));
else
return BinaryStringRebuilder.Create(this.GetSubText(left),
BinaryStringRebuilder.Create(text, this.GetSubText(right)));
}
#endregion
}
}