// // 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 } }