aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document
diff options
context:
space:
mode:
authorVictoria Plitt <Victoria.Plitt@twine-s.com>2019-04-08 13:49:55 +0300
committerVictoria Plitt <Victoria.Plitt@twine-s.com>2019-04-08 13:49:55 +0300
commitfc8a05358a92cc3c77c5f1e30d536807ef0614fd (patch)
treec65f696ebd60f3790145721307c255e5a339923f /Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document
parentb4a71931ea52636c6b36376aa9d71697ccf73524 (diff)
downloadTango-fc8a05358a92cc3c77c5f1e30d536807ef0614fd.tar.gz
Tango-fc8a05358a92cc3c77c5f1e30d536807ef0614fd.zip
were added scripting projects
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document')
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ChangeTrackingCheckpoint.cs140
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentChangeEventArgs.cs131
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentChangeOperation.cs52
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentLine.cs242
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentLineTree.cs712
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/GapTextBuffer.cs192
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ILineTracker.cs55
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ISegment.cs219
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ITextSource.cs320
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/IUndoableOperation.cs30
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/LineManager.cs288
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/LineNode.cs83
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/NewLineFinder.cs134
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/OffsetChangeMap.cs347
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchor.cs194
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchorNode.cs87
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchorTree.cs753
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs836
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocumentWeakEventManager.cs149
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextLocation.cs166
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextSegment.cs248
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextSegmentCollection.cs971
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextUtilities.cs332
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoOperationGroup.cs61
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoStack.cs450
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/WeakLineTracker.cs85
26 files changed, 7277 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ChangeTrackingCheckpoint.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ChangeTrackingCheckpoint.cs
new file mode 100644
index 000000000..283731da8
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ChangeTrackingCheckpoint.cs
@@ -0,0 +1,140 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using Tango.Scripting.Editors.Utils;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// <para>A checkpoint that allows tracking changes to a TextDocument.</para>
+ /// <para>
+ /// Use <see cref="TextDocument.CreateSnapshot(out ChangeTrackingCheckpoint)"/> to create a checkpoint.
+ /// </para>
+ /// </summary>
+ /// <remarks>
+ /// <para>The <see cref="ChangeTrackingCheckpoint"/> class allows tracking document changes, even from background threads.</para>
+ /// <para>Once you have two checkpoints, you can call <see cref="ChangeTrackingCheckpoint.GetChangesTo"/> to retrieve the complete list
+ /// of document changes that happened between those versions of the document.</para>
+ /// </remarks>
+ public sealed class ChangeTrackingCheckpoint
+ {
+ // Object that is unique per document.
+ // Used to determine if two checkpoints belong to the same document.
+ // We don't use a reference to the document itself to allow the GC to reclaim the document memory
+ // even if there are still references to checkpoints.
+ readonly object documentIdentifier;
+
+ // 'value' is the change from the previous checkpoint to this checkpoint
+ // TODO: store the change in the older checkpoint instead - if only a reference to the
+ // newest document version exists, the GC should be able to collect all DocumentChangeEventArgs.
+ readonly DocumentChangeEventArgs value;
+ readonly int id;
+ ChangeTrackingCheckpoint next;
+
+ internal ChangeTrackingCheckpoint(object documentIdentifier)
+ {
+ this.documentIdentifier = documentIdentifier;
+ }
+
+ internal ChangeTrackingCheckpoint(object documentIdentifier, DocumentChangeEventArgs value, int id)
+ {
+ this.documentIdentifier = documentIdentifier;
+ this.value = value;
+ this.id = id;
+ }
+
+ internal ChangeTrackingCheckpoint Append(DocumentChangeEventArgs change)
+ {
+ Debug.Assert(this.next == null);
+ this.next = new ChangeTrackingCheckpoint(this.documentIdentifier, change, unchecked( this.id + 1 ));
+ return this.next;
+ }
+
+ /// <summary>
+ /// Creates a change tracking checkpoint for the specified document.
+ /// This method is thread-safe.
+ /// If you need a ChangeTrackingCheckpoint that's consistent with a snapshot of the document,
+ /// use <see cref="TextDocument.CreateSnapshot(out ChangeTrackingCheckpoint)"/>.
+ /// </summary>
+ public static ChangeTrackingCheckpoint Create(TextDocument document)
+ {
+ if (document == null)
+ throw new ArgumentNullException("document");
+ return document.CreateChangeTrackingCheckpoint();
+ }
+
+ /// <summary>
+ /// Gets whether this checkpoint belongs to the same document as the other checkpoint.
+ /// </summary>
+ public bool BelongsToSameDocumentAs(ChangeTrackingCheckpoint other)
+ {
+ if (other == null)
+ throw new ArgumentNullException("other");
+ return documentIdentifier == other.documentIdentifier;
+ }
+
+ /// <summary>
+ /// Compares the age of this checkpoint to the other checkpoint.
+ /// </summary>
+ /// <remarks>This method is thread-safe.</remarks>
+ /// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this checkpoint.</exception>
+ /// <returns>-1 if this checkpoint is older than <paramref name="other"/>.
+ /// 0 if <c>this</c>==<paramref name="other"/>.
+ /// 1 if this checkpoint is newer than <paramref name="other"/>.</returns>
+ public int CompareAge(ChangeTrackingCheckpoint other)
+ {
+ if (other == null)
+ throw new ArgumentNullException("other");
+ if (other.documentIdentifier != this.documentIdentifier)
+ throw new ArgumentException("Checkpoints do not belong to the same document.");
+ // We will allow overflows, but assume that the maximum distance between checkpoints is 2^31-1.
+ // This is guaranteed on x86 because so many checkpoints don't fit into memory.
+ return Math.Sign(unchecked( this.id - other.id ));
+ }
+
+ /// <summary>
+ /// Gets the changes from this checkpoint to the other checkpoint.
+ /// If 'other' is older than this checkpoint, reverse changes are calculated.
+ /// </summary>
+ /// <remarks>This method is thread-safe.</remarks>
+ /// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this checkpoint.</exception>
+ public IEnumerable<DocumentChangeEventArgs> GetChangesTo(ChangeTrackingCheckpoint other)
+ {
+ int result = CompareAge(other);
+ if (result < 0)
+ return GetForwardChanges(other);
+ else if (result > 0)
+ return other.GetForwardChanges(this).Reverse().Select(change => change.Invert());
+ else
+ return Empty<DocumentChangeEventArgs>.Array;
+ }
+
+ IEnumerable<DocumentChangeEventArgs> GetForwardChanges(ChangeTrackingCheckpoint other)
+ {
+ // Return changes from this(exclusive) to other(inclusive).
+ ChangeTrackingCheckpoint node = this;
+ do {
+ node = node.next;
+ yield return node.value;
+ } while (node != other);
+ }
+
+ /// <summary>
+ /// Calculates where the offset has moved in the other buffer version.
+ /// </summary>
+ /// <remarks>This method is thread-safe.</remarks>
+ /// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this checkpoint.</exception>
+ public int MoveOffsetTo(ChangeTrackingCheckpoint other, int oldOffset, AnchorMovementType movement)
+ {
+ int offset = oldOffset;
+ foreach (DocumentChangeEventArgs e in GetChangesTo(other)) {
+ offset = e.GetNewOffset(offset, movement);
+ }
+ return offset;
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentChangeEventArgs.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentChangeEventArgs.cs
new file mode 100644
index 000000000..17307680a
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentChangeEventArgs.cs
@@ -0,0 +1,131 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using Tango.Scripting.Editors.Utils;
+using System;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Describes a change of the document text.
+ /// This class is thread-safe.
+ /// </summary>
+ [Serializable]
+ public class DocumentChangeEventArgs : EventArgs
+ {
+ /// <summary>
+ /// The offset at which the change occurs.
+ /// </summary>
+ public int Offset { get; private set; }
+
+ /// <summary>
+ /// The text that was removed.
+ /// </summary>
+ public string RemovedText { get; private set; }
+
+ /// <summary>
+ /// The number of characters removed.
+ /// </summary>
+ public int RemovalLength {
+ get { return RemovedText.Length; }
+ }
+
+ /// <summary>
+ /// The text that was inserted.
+ /// </summary>
+ public string InsertedText { get; private set; }
+
+ /// <summary>
+ /// The number of characters inserted.
+ /// </summary>
+ public int InsertionLength {
+ get { return InsertedText.Length; }
+ }
+
+ volatile OffsetChangeMap offsetChangeMap;
+
+ /// <summary>
+ /// Gets the OffsetChangeMap associated with this document change.
+ /// </summary>
+ /// <remarks>The OffsetChangeMap instance is guaranteed to be frozen and thus thread-safe.</remarks>
+ public OffsetChangeMap OffsetChangeMap {
+ get {
+ OffsetChangeMap map = offsetChangeMap;
+ if (map == null) {
+ // create OffsetChangeMap on demand
+ map = OffsetChangeMap.FromSingleElement(CreateSingleChangeMapEntry());
+ offsetChangeMap = map;
+ }
+ return map;
+ }
+ }
+
+ internal OffsetChangeMapEntry CreateSingleChangeMapEntry()
+ {
+ return new OffsetChangeMapEntry(this.Offset, this.RemovalLength, this.InsertionLength);
+ }
+
+ /// <summary>
+ /// Gets the OffsetChangeMap, or null if the default offset map (=single replacement) is being used.
+ /// </summary>
+ internal OffsetChangeMap OffsetChangeMapOrNull {
+ get {
+ return offsetChangeMap;
+ }
+ }
+
+ /// <summary>
+ /// Gets the new offset where the specified offset moves after this document change.
+ /// </summary>
+ public int GetNewOffset(int offset, AnchorMovementType movementType)
+ {
+ if (offsetChangeMap != null)
+ return offsetChangeMap.GetNewOffset(offset, movementType);
+ else
+ return CreateSingleChangeMapEntry().GetNewOffset(offset, movementType);
+ }
+
+ /// <summary>
+ /// Creates a new DocumentChangeEventArgs object.
+ /// </summary>
+ public DocumentChangeEventArgs(int offset, string removedText, string insertedText)
+ : this(offset, removedText, insertedText, null)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new DocumentChangeEventArgs object.
+ /// </summary>
+ public DocumentChangeEventArgs(int offset, string removedText, string insertedText, OffsetChangeMap offsetChangeMap)
+ {
+ ThrowUtil.CheckNotNegative(offset, "offset");
+ ThrowUtil.CheckNotNull(removedText, "removedText");
+ ThrowUtil.CheckNotNull(insertedText, "insertedText");
+
+ this.Offset = offset;
+ this.RemovedText = removedText;
+ this.InsertedText = insertedText;
+
+ if (offsetChangeMap != null) {
+ if (!offsetChangeMap.IsFrozen)
+ throw new ArgumentException("The OffsetChangeMap must be frozen before it can be used in DocumentChangeEventArgs");
+ if (!offsetChangeMap.IsValidForDocumentChange(offset, removedText.Length, insertedText.Length))
+ throw new ArgumentException("OffsetChangeMap is not valid for this document change", "offsetChangeMap");
+ this.offsetChangeMap = offsetChangeMap;
+ }
+ }
+
+ /// <summary>
+ /// Creates DocumentChangeEventArgs for the reverse change.
+ /// </summary>
+ public DocumentChangeEventArgs Invert()
+ {
+ OffsetChangeMap map = this.OffsetChangeMapOrNull;
+ if (map != null) {
+ map = map.Invert();
+ map.Freeze();
+ }
+ return new DocumentChangeEventArgs(this.Offset, this.InsertedText, this.RemovedText, map);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentChangeOperation.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentChangeOperation.cs
new file mode 100644
index 000000000..f24b7ed4d
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentChangeOperation.cs
@@ -0,0 +1,52 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Diagnostics;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Describes a change to a TextDocument.
+ /// </summary>
+ sealed class DocumentChangeOperation : IUndoableOperationWithContext
+ {
+ TextDocument document;
+ DocumentChangeEventArgs change;
+
+ public DocumentChangeOperation(TextDocument document, DocumentChangeEventArgs change)
+ {
+ this.document = document;
+ this.change = change;
+ }
+
+ public void Undo(UndoStack stack)
+ {
+ Debug.Assert(stack.state == UndoStack.StatePlayback);
+ stack.RegisterAffectedDocument(document);
+ stack.state = UndoStack.StatePlaybackModifyDocument;
+ this.Undo();
+ stack.state = UndoStack.StatePlayback;
+ }
+
+ public void Redo(UndoStack stack)
+ {
+ Debug.Assert(stack.state == UndoStack.StatePlayback);
+ stack.RegisterAffectedDocument(document);
+ stack.state = UndoStack.StatePlaybackModifyDocument;
+ this.Redo();
+ stack.state = UndoStack.StatePlayback;
+ }
+
+ public void Undo()
+ {
+ OffsetChangeMap map = change.OffsetChangeMapOrNull;
+ document.Replace(change.Offset, change.InsertionLength, change.RemovedText, map != null ? map.Invert() : null);
+ }
+
+ public void Redo()
+ {
+ document.Replace(change.Offset, change.RemovalLength, change.InsertedText, change.OffsetChangeMapOrNull);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentLine.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentLine.cs
new file mode 100644
index 000000000..746f31637
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentLine.cs
@@ -0,0 +1,242 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Diagnostics;
+using System.Globalization;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Represents a line inside a <see cref="TextDocument"/>.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The <see cref="TextDocument.Lines"/> collection contains one DocumentLine instance
+ /// for every line in the document. This collection is read-only to user code and is automatically
+ /// updated to reflect the current document content.
+ /// </para>
+ /// <para>
+ /// Internally, the DocumentLine instances are arranged in a binary tree that allows for both efficient updates and lookup.
+ /// Converting between offset and line number is possible in O(lg N) time,
+ /// and the data structure also updates all offsets in O(lg N) whenever a line is inserted or removed.
+ /// </para>
+ /// </remarks>
+ public sealed partial class DocumentLine : ISegment
+ {
+ #region Constructor
+ #if DEBUG
+ // Required for thread safety check which is done only in debug builds.
+ // To save space, we don't store the document reference in release builds as we don't need it there.
+ readonly TextDocument document;
+ #endif
+
+ internal bool isDeleted;
+
+ internal DocumentLine(TextDocument document)
+ {
+ #if DEBUG
+ Debug.Assert(document != null);
+ this.document = document;
+ #endif
+ }
+
+ [Conditional("DEBUG")]
+ void DebugVerifyAccess()
+ {
+ #if DEBUG
+ document.DebugVerifyAccess();
+ #endif
+ }
+ #endregion
+
+ #region Events
+// /// <summary>
+// /// Is raised when the line is deleted.
+// /// </summary>
+// public event EventHandler Deleted;
+//
+// /// <summary>
+// /// Is raised when the line's text changes.
+// /// </summary>
+// public event EventHandler TextChanged;
+//
+// /// <summary>
+// /// Raises the Deleted or TextChanged event.
+// /// </summary>
+// internal void RaiseChanged()
+// {
+// if (IsDeleted) {
+// if (Deleted != null)
+// Deleted(this, EventArgs.Empty);
+// } else {
+// if (TextChanged != null)
+// TextChanged(this, EventArgs.Empty);
+// }
+// }
+ #endregion
+
+ #region Properties stored in tree
+ /// <summary>
+ /// Gets if this line was deleted from the document.
+ /// </summary>
+ public bool IsDeleted {
+ get {
+ DebugVerifyAccess();
+ return isDeleted;
+ }
+ }
+
+ /// <summary>
+ /// Gets the number of this line.
+ /// Runtime: O(log n)
+ /// </summary>
+ /// <exception cref="InvalidOperationException">The line was deleted.</exception>
+ public int LineNumber {
+ get {
+ if (IsDeleted)
+ throw new InvalidOperationException();
+ return DocumentLineTree.GetIndexFromNode(this) + 1;
+ }
+ }
+
+ /// <summary>
+ /// Gets the starting offset of the line in the document's text.
+ /// Runtime: O(log n)
+ /// </summary>
+ /// <exception cref="InvalidOperationException">The line was deleted.</exception>
+ public int Offset {
+ get {
+ if (IsDeleted)
+ throw new InvalidOperationException();
+ return DocumentLineTree.GetOffsetFromNode(this);
+ }
+ }
+
+ /// <summary>
+ /// Gets the end offset of the line in the document's text (the offset before the line delimiter).
+ /// Runtime: O(log n)
+ /// </summary>
+ /// <exception cref="InvalidOperationException">The line was deleted.</exception>
+ /// <remarks>EndOffset = <see cref="Offset"/> + <see cref="Length"/>.</remarks>
+ public int EndOffset {
+ get { return this.Offset + this.Length; }
+ }
+ #endregion
+
+ #region Length
+ int totalLength;
+ byte delimiterLength;
+
+ /// <summary>
+ /// Gets the length of this line. The length does not include the line delimiter. O(1)
+ /// </summary>
+ /// <remarks>This property is still available even if the line was deleted;
+ /// in that case, it contains the line's length before the deletion.</remarks>
+ public int Length {
+ get {
+ DebugVerifyAccess();
+ return totalLength - delimiterLength;
+ }
+ }
+
+ /// <summary>
+ /// Gets the length of this line, including the line delimiter. O(1)
+ /// </summary>
+ /// <remarks>This property is still available even if the line was deleted;
+ /// in that case, it contains the line's length before the deletion.</remarks>
+ public int TotalLength {
+ get {
+ DebugVerifyAccess();
+ return totalLength;
+ }
+ internal set {
+ // this is set by DocumentLineTree
+ totalLength = value;
+ }
+ }
+
+ /// <summary>
+ /// <para>Gets the length of the line delimiter.</para>
+ /// <para>The value is 1 for single <c>"\r"</c> or <c>"\n"</c>, 2 for the <c>"\r\n"</c> sequence;
+ /// and 0 for the last line in the document.</para>
+ /// </summary>
+ /// <remarks>This property is still available even if the line was deleted;
+ /// in that case, it contains the line delimiter's length before the deletion.</remarks>
+ public int DelimiterLength {
+ get {
+ DebugVerifyAccess();
+ return delimiterLength;
+ }
+ internal set {
+ Debug.Assert(value >= 0 && value <= 2);
+ delimiterLength = (byte)value;
+ }
+ }
+ #endregion
+
+ #region Previous / Next Line
+ /// <summary>
+ /// Gets the next line in the document.
+ /// </summary>
+ /// <returns>The line following this line, or null if this is the last line.</returns>
+ public DocumentLine NextLine {
+ get {
+ DebugVerifyAccess();
+
+ if (right != null) {
+ return right.LeftMost;
+ } else {
+ DocumentLine node = this;
+ DocumentLine oldNode;
+ do {
+ oldNode = node;
+ node = node.parent;
+ // we are on the way up from the right part, don't output node again
+ } while (node != null && node.right == oldNode);
+ return node;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the previous line in the document.
+ /// </summary>
+ /// <returns>The line before this line, or null if this is the first line.</returns>
+ public DocumentLine PreviousLine {
+ get {
+ DebugVerifyAccess();
+
+ if (left != null) {
+ return left.RightMost;
+ } else {
+ DocumentLine node = this;
+ DocumentLine oldNode;
+ do {
+ oldNode = node;
+ node = node.parent;
+ // we are on the way up from the left part, don't output node again
+ } while (node != null && node.left == oldNode);
+ return node;
+ }
+ }
+ }
+ #endregion
+
+ #region ToString
+ /// <summary>
+ /// Gets a string with debug output showing the line number and offset.
+ /// Does not include the line's text.
+ /// </summary>
+ public override string ToString()
+ {
+ if (IsDeleted)
+ return "[DocumentLine deleted]";
+ else
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "[DocumentLine Number={0} Offset={1} Length={2}]", LineNumber, Offset, Length);
+ }
+ #endregion
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentLineTree.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentLineTree.cs
new file mode 100644
index 000000000..927dc3e01
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/DocumentLineTree.cs
@@ -0,0 +1,712 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+namespace Tango.Scripting.Editors.Document
+{
+ using LineNode = DocumentLine;
+
+ /// <summary>
+ /// Data structure for efficient management of the document lines (most operations are O(lg n)).
+ /// This implements an augmented red-black tree.
+ /// See <see cref="LineNode"/> for the augmented data.
+ ///
+ /// NOTE: The tree is never empty, initially it contains an empty line.
+ /// </summary>
+ sealed class DocumentLineTree : IList<DocumentLine>
+ {
+ #region Constructor
+ readonly TextDocument document;
+ LineNode root;
+
+ public DocumentLineTree(TextDocument document)
+ {
+ this.document = document;
+
+ DocumentLine emptyLine = new DocumentLine(document);
+ root = emptyLine.InitLineNode();
+ }
+ #endregion
+
+ #region Rotation callbacks
+ internal static void UpdateAfterChildrenChange(LineNode node)
+ {
+ int totalCount = 1;
+ int totalLength = node.TotalLength;
+ if (node.left != null) {
+ totalCount += node.left.nodeTotalCount;
+ totalLength += node.left.nodeTotalLength;
+ }
+ if (node.right != null) {
+ totalCount += node.right.nodeTotalCount;
+ totalLength += node.right.nodeTotalLength;
+ }
+ if (totalCount != node.nodeTotalCount
+ || totalLength != node.nodeTotalLength)
+ {
+ node.nodeTotalCount = totalCount;
+ node.nodeTotalLength = totalLength;
+ if (node.parent != null) UpdateAfterChildrenChange(node.parent);
+ }
+ }
+
+ static void UpdateAfterRotateLeft(LineNode node)
+ {
+ UpdateAfterChildrenChange(node);
+
+ // not required: rotations only happen on insertions/deletions
+ // -> totalCount changes -> the parent is always updated
+ //UpdateAfterChildrenChange(node.parent);
+ }
+
+ static void UpdateAfterRotateRight(LineNode node)
+ {
+ UpdateAfterChildrenChange(node);
+
+ // not required: rotations only happen on insertions/deletions
+ // -> totalCount changes -> the parent is always updated
+ //UpdateAfterChildrenChange(node.parent);
+ }
+ #endregion
+
+ #region RebuildDocument
+ /// <summary>
+ /// Rebuild the tree, in O(n).
+ /// </summary>
+ public void RebuildTree(List<DocumentLine> documentLines)
+ {
+ LineNode[] nodes = new LineNode[documentLines.Count];
+ for (int i = 0; i < documentLines.Count; i++) {
+ DocumentLine ls = documentLines[i];
+ LineNode node = ls.InitLineNode();
+ nodes[i] = node;
+ }
+ Debug.Assert(nodes.Length > 0);
+ // now build the corresponding balanced tree
+ int height = GetTreeHeight(nodes.Length);
+ Debug.WriteLine("DocumentLineTree will have height: " + height);
+ root = BuildTree(nodes, 0, nodes.Length, height);
+ root.color = BLACK;
+ #if DEBUG
+ CheckProperties();
+ #endif
+ }
+
+ internal static int GetTreeHeight(int size)
+ {
+ if (size == 0)
+ return 0;
+ else
+ return GetTreeHeight(size / 2) + 1;
+ }
+
+ /// <summary>
+ /// build a tree from a list of nodes
+ /// </summary>
+ LineNode BuildTree(LineNode[] nodes, int start, int end, int subtreeHeight)
+ {
+ Debug.Assert(start <= end);
+ if (start == end) {
+ return null;
+ }
+ int middle = (start + end) / 2;
+ LineNode node = nodes[middle];
+ node.left = BuildTree(nodes, start, middle, subtreeHeight - 1);
+ node.right = BuildTree(nodes, middle + 1, end, subtreeHeight - 1);
+ if (node.left != null) node.left.parent = node;
+ if (node.right != null) node.right.parent = node;
+ if (subtreeHeight == 1)
+ node.color = RED;
+ UpdateAfterChildrenChange(node);
+ return node;
+ }
+ #endregion
+
+ #region GetNodeBy... / Get...FromNode
+ LineNode GetNodeByIndex(int index)
+ {
+ Debug.Assert(index >= 0);
+ Debug.Assert(index < root.nodeTotalCount);
+ LineNode node = root;
+ while (true) {
+ if (node.left != null && index < node.left.nodeTotalCount) {
+ node = node.left;
+ } else {
+ if (node.left != null) {
+ index -= node.left.nodeTotalCount;
+ }
+ if (index == 0)
+ return node;
+ index--;
+ node = node.right;
+ }
+ }
+ }
+
+ internal static int GetIndexFromNode(LineNode node)
+ {
+ int index = (node.left != null) ? node.left.nodeTotalCount : 0;
+ while (node.parent != null) {
+ if (node == node.parent.right) {
+ if (node.parent.left != null)
+ index += node.parent.left.nodeTotalCount;
+ index++;
+ }
+ node = node.parent;
+ }
+ return index;
+ }
+
+ LineNode GetNodeByOffset(int offset)
+ {
+ Debug.Assert(offset >= 0);
+ Debug.Assert(offset <= root.nodeTotalLength);
+ if (offset == root.nodeTotalLength) {
+ return root.RightMost;
+ }
+ LineNode node = root;
+ while (true) {
+ if (node.left != null && offset < node.left.nodeTotalLength) {
+ node = node.left;
+ } else {
+ if (node.left != null) {
+ offset -= node.left.nodeTotalLength;
+ }
+ offset -= node.TotalLength;
+ if (offset < 0)
+ return node;
+ node = node.right;
+ }
+ }
+ }
+
+ internal static int GetOffsetFromNode(LineNode node)
+ {
+ int offset = (node.left != null) ? node.left.nodeTotalLength : 0;
+ while (node.parent != null) {
+ if (node == node.parent.right) {
+ if (node.parent.left != null)
+ offset += node.parent.left.nodeTotalLength;
+ offset += node.parent.TotalLength;
+ }
+ node = node.parent;
+ }
+ return offset;
+ }
+ #endregion
+
+ #region GetLineBy
+ public DocumentLine GetByNumber(int number)
+ {
+ return GetNodeByIndex(number - 1);
+ }
+
+ public DocumentLine GetByOffset(int offset)
+ {
+ return GetNodeByOffset(offset);
+ }
+ #endregion
+
+ #region LineCount
+ public int LineCount {
+ get {
+ return root.nodeTotalCount;
+ }
+ }
+ #endregion
+
+ #region CheckProperties
+ #if DEBUG
+ [Conditional("DATACONSISTENCYTEST")]
+ internal void CheckProperties()
+ {
+ Debug.Assert(root.nodeTotalLength == document.TextLength);
+ CheckProperties(root);
+
+ // check red-black property:
+ int blackCount = -1;
+ CheckNodeProperties(root, null, RED, 0, ref blackCount);
+ }
+
+ void CheckProperties(LineNode node)
+ {
+ int totalCount = 1;
+ int totalLength = node.TotalLength;
+ if (node.left != null) {
+ CheckProperties(node.left);
+ totalCount += node.left.nodeTotalCount;
+ totalLength += node.left.nodeTotalLength;
+ }
+ if (node.right != null) {
+ CheckProperties(node.right);
+ totalCount += node.right.nodeTotalCount;
+ totalLength += node.right.nodeTotalLength;
+ }
+ Debug.Assert(node.nodeTotalCount == totalCount);
+ Debug.Assert(node.nodeTotalLength == totalLength);
+ }
+
+ /*
+ 1. A node is either red or black.
+ 2. The root is black.
+ 3. All leaves are black. (The leaves are the NIL children.)
+ 4. Both children of every red node are black. (So every red node must have a black parent.)
+ 5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.)
+ */
+ void CheckNodeProperties(LineNode node, LineNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount)
+ {
+ if (node == null) return;
+
+ Debug.Assert(node.parent == parentNode);
+
+ if (parentColor == RED) {
+ Debug.Assert(node.color == BLACK);
+ }
+ if (node.color == BLACK) {
+ blackCount++;
+ }
+ if (node.left == null && node.right == null) {
+ // node is a leaf node:
+ if (expectedBlackCount == -1)
+ expectedBlackCount = blackCount;
+ else
+ Debug.Assert(expectedBlackCount == blackCount);
+ }
+ CheckNodeProperties(node.left, node, node.color, blackCount, ref expectedBlackCount);
+ CheckNodeProperties(node.right, node, node.color, blackCount, ref expectedBlackCount);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ public string GetTreeAsString()
+ {
+ StringBuilder b = new StringBuilder();
+ AppendTreeToString(root, b, 0);
+ return b.ToString();
+ }
+
+ static void AppendTreeToString(LineNode node, StringBuilder b, int indent)
+ {
+ if (node.color == RED)
+ b.Append("RED ");
+ else
+ b.Append("BLACK ");
+ b.AppendLine(node.ToString());
+ indent += 2;
+ if (node.left != null) {
+ b.Append(' ', indent);
+ b.Append("L: ");
+ AppendTreeToString(node.left, b, indent);
+ }
+ if (node.right != null) {
+ b.Append(' ', indent);
+ b.Append("R: ");
+ AppendTreeToString(node.right, b, indent);
+ }
+ }
+ #endif
+ #endregion
+
+ #region Insert/Remove lines
+ public void RemoveLine(DocumentLine line)
+ {
+ RemoveNode(line);
+ line.isDeleted = true;
+ }
+
+ public DocumentLine InsertLineAfter(DocumentLine line, int totalLength)
+ {
+ DocumentLine newLine = new DocumentLine(document);
+ newLine.TotalLength = totalLength;
+
+ InsertAfter(line, newLine);
+ return newLine;
+ }
+
+ void InsertAfter(LineNode node, DocumentLine newLine)
+ {
+ LineNode newNode = newLine.InitLineNode();
+ if (node.right == null) {
+ InsertAsRight(node, newNode);
+ } else {
+ InsertAsLeft(node.right.LeftMost, newNode);
+ }
+ }
+ #endregion
+
+ #region Red/Black Tree
+ internal const bool RED = true;
+ internal const bool BLACK = false;
+
+ void InsertAsLeft(LineNode parentNode, LineNode newNode)
+ {
+ Debug.Assert(parentNode.left == null);
+ parentNode.left = newNode;
+ newNode.parent = parentNode;
+ newNode.color = RED;
+ UpdateAfterChildrenChange(parentNode);
+ FixTreeOnInsert(newNode);
+ }
+
+ void InsertAsRight(LineNode parentNode, LineNode newNode)
+ {
+ Debug.Assert(parentNode.right == null);
+ parentNode.right = newNode;
+ newNode.parent = parentNode;
+ newNode.color = RED;
+ UpdateAfterChildrenChange(parentNode);
+ FixTreeOnInsert(newNode);
+ }
+
+ void FixTreeOnInsert(LineNode node)
+ {
+ Debug.Assert(node != null);
+ Debug.Assert(node.color == RED);
+ Debug.Assert(node.left == null || node.left.color == BLACK);
+ Debug.Assert(node.right == null || node.right.color == BLACK);
+
+ LineNode parentNode = node.parent;
+ if (parentNode == null) {
+ // we inserted in the root -> the node must be black
+ // since this is a root node, making the node black increments the number of black nodes
+ // on all paths by one, so it is still the same for all paths.
+ node.color = BLACK;
+ return;
+ }
+ if (parentNode.color == BLACK) {
+ // if the parent node where we inserted was black, our red node is placed correctly.
+ // since we inserted a red node, the number of black nodes on each path is unchanged
+ // -> the tree is still balanced
+ return;
+ }
+ // parentNode is red, so there is a conflict here!
+
+ // because the root is black, parentNode is not the root -> there is a grandparent node
+ LineNode grandparentNode = parentNode.parent;
+ LineNode uncleNode = Sibling(parentNode);
+ if (uncleNode != null && uncleNode.color == RED) {
+ parentNode.color = BLACK;
+ uncleNode.color = BLACK;
+ grandparentNode.color = RED;
+ FixTreeOnInsert(grandparentNode);
+ return;
+ }
+ // now we know: parent is red but uncle is black
+ // First rotation:
+ if (node == parentNode.right && parentNode == grandparentNode.left) {
+ RotateLeft(parentNode);
+ node = node.left;
+ } else if (node == parentNode.left && parentNode == grandparentNode.right) {
+ RotateRight(parentNode);
+ node = node.right;
+ }
+ // because node might have changed, reassign variables:
+ parentNode = node.parent;
+ grandparentNode = parentNode.parent;
+
+ // Now recolor a bit:
+ parentNode.color = BLACK;
+ grandparentNode.color = RED;
+ // Second rotation:
+ if (node == parentNode.left && parentNode == grandparentNode.left) {
+ RotateRight(grandparentNode);
+ } else {
+ // because of the first rotation, this is guaranteed:
+ Debug.Assert(node == parentNode.right && parentNode == grandparentNode.right);
+ RotateLeft(grandparentNode);
+ }
+ }
+
+ void RemoveNode(LineNode removedNode)
+ {
+ if (removedNode.left != null && removedNode.right != null) {
+ // replace removedNode with it's in-order successor
+
+ LineNode leftMost = removedNode.right.LeftMost;
+ RemoveNode(leftMost); // remove leftMost from its current location
+
+ // and overwrite the removedNode with it
+ ReplaceNode(removedNode, leftMost);
+ leftMost.left = removedNode.left;
+ if (leftMost.left != null) leftMost.left.parent = leftMost;
+ leftMost.right = removedNode.right;
+ if (leftMost.right != null) leftMost.right.parent = leftMost;
+ leftMost.color = removedNode.color;
+
+ UpdateAfterChildrenChange(leftMost);
+ if (leftMost.parent != null) UpdateAfterChildrenChange(leftMost.parent);
+ return;
+ }
+
+ // now either removedNode.left or removedNode.right is null
+ // get the remaining child
+ LineNode parentNode = removedNode.parent;
+ LineNode childNode = removedNode.left ?? removedNode.right;
+ ReplaceNode(removedNode, childNode);
+ if (parentNode != null) UpdateAfterChildrenChange(parentNode);
+ if (removedNode.color == BLACK) {
+ if (childNode != null && childNode.color == RED) {
+ childNode.color = BLACK;
+ } else {
+ FixTreeOnDelete(childNode, parentNode);
+ }
+ }
+ }
+
+ void FixTreeOnDelete(LineNode node, LineNode parentNode)
+ {
+ Debug.Assert(node == null || node.parent == parentNode);
+ if (parentNode == null)
+ return;
+
+ // warning: node may be null
+ LineNode sibling = Sibling(node, parentNode);
+ if (sibling.color == RED) {
+ parentNode.color = RED;
+ sibling.color = BLACK;
+ if (node == parentNode.left) {
+ RotateLeft(parentNode);
+ } else {
+ RotateRight(parentNode);
+ }
+
+ sibling = Sibling(node, parentNode); // update value of sibling after rotation
+ }
+
+ if (parentNode.color == BLACK
+ && sibling.color == BLACK
+ && GetColor(sibling.left) == BLACK
+ && GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ FixTreeOnDelete(parentNode, parentNode.parent);
+ return;
+ }
+
+ if (parentNode.color == RED
+ && sibling.color == BLACK
+ && GetColor(sibling.left) == BLACK
+ && GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ parentNode.color = BLACK;
+ return;
+ }
+
+ if (node == parentNode.left &&
+ sibling.color == BLACK &&
+ GetColor(sibling.left) == RED &&
+ GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ sibling.left.color = BLACK;
+ RotateRight(sibling);
+ }
+ else if (node == parentNode.right &&
+ sibling.color == BLACK &&
+ GetColor(sibling.right) == RED &&
+ GetColor(sibling.left) == BLACK)
+ {
+ sibling.color = RED;
+ sibling.right.color = BLACK;
+ RotateLeft(sibling);
+ }
+ sibling = Sibling(node, parentNode); // update value of sibling after rotation
+
+ sibling.color = parentNode.color;
+ parentNode.color = BLACK;
+ if (node == parentNode.left) {
+ if (sibling.right != null) {
+ Debug.Assert(sibling.right.color == RED);
+ sibling.right.color = BLACK;
+ }
+ RotateLeft(parentNode);
+ } else {
+ if (sibling.left != null) {
+ Debug.Assert(sibling.left.color == RED);
+ sibling.left.color = BLACK;
+ }
+ RotateRight(parentNode);
+ }
+ }
+
+ void ReplaceNode(LineNode replacedNode, LineNode newNode)
+ {
+ if (replacedNode.parent == null) {
+ Debug.Assert(replacedNode == root);
+ root = newNode;
+ } else {
+ if (replacedNode.parent.left == replacedNode)
+ replacedNode.parent.left = newNode;
+ else
+ replacedNode.parent.right = newNode;
+ }
+ if (newNode != null) {
+ newNode.parent = replacedNode.parent;
+ }
+ replacedNode.parent = null;
+ }
+
+ void RotateLeft(LineNode p)
+ {
+ // let q be p's right child
+ LineNode q = p.right;
+ Debug.Assert(q != null);
+ Debug.Assert(q.parent == p);
+ // set q to be the new root
+ ReplaceNode(p, q);
+
+ // set p's right child to be q's left child
+ p.right = q.left;
+ if (p.right != null) p.right.parent = p;
+ // set q's left child to be p
+ q.left = p;
+ p.parent = q;
+ UpdateAfterRotateLeft(p);
+ }
+
+ void RotateRight(LineNode p)
+ {
+ // let q be p's left child
+ LineNode q = p.left;
+ Debug.Assert(q != null);
+ Debug.Assert(q.parent == p);
+ // set q to be the new root
+ ReplaceNode(p, q);
+
+ // set p's left child to be q's right child
+ p.left = q.right;
+ if (p.left != null) p.left.parent = p;
+ // set q's right child to be p
+ q.right = p;
+ p.parent = q;
+ UpdateAfterRotateRight(p);
+ }
+
+ static LineNode Sibling(LineNode node)
+ {
+ if (node == node.parent.left)
+ return node.parent.right;
+ else
+ return node.parent.left;
+ }
+
+ static LineNode Sibling(LineNode node, LineNode parentNode)
+ {
+ Debug.Assert(node == null || node.parent == parentNode);
+ if (node == parentNode.left)
+ return parentNode.right;
+ else
+ return parentNode.left;
+ }
+
+ static bool GetColor(LineNode node)
+ {
+ return node != null ? node.color : BLACK;
+ }
+ #endregion
+
+ #region IList implementation
+ DocumentLine IList<DocumentLine>.this[int index] {
+ get {
+ document.VerifyAccess();
+ return GetByNumber(1 + index);
+ }
+ set {
+ throw new NotSupportedException();
+ }
+ }
+
+ int ICollection<DocumentLine>.Count {
+ get {
+ document.VerifyAccess();
+ return LineCount;
+ }
+ }
+
+ bool ICollection<DocumentLine>.IsReadOnly {
+ get { return true; }
+ }
+
+ int IList<DocumentLine>.IndexOf(DocumentLine item)
+ {
+ document.VerifyAccess();
+ if (item == null || item.IsDeleted)
+ return -1;
+ int index = item.LineNumber - 1;
+ if (index < LineCount && GetNodeByIndex(index) == item)
+ return index;
+ else
+ return -1;
+ }
+
+ void IList<DocumentLine>.Insert(int index, DocumentLine item)
+ {
+ throw new NotSupportedException();
+ }
+
+ void IList<DocumentLine>.RemoveAt(int index)
+ {
+ throw new NotSupportedException();
+ }
+
+ void ICollection<DocumentLine>.Add(DocumentLine item)
+ {
+ throw new NotSupportedException();
+ }
+
+ void ICollection<DocumentLine>.Clear()
+ {
+ throw new NotSupportedException();
+ }
+
+ bool ICollection<DocumentLine>.Contains(DocumentLine item)
+ {
+ IList<DocumentLine> self = this;
+ return self.IndexOf(item) >= 0;
+ }
+
+ void ICollection<DocumentLine>.CopyTo(DocumentLine[] array, int arrayIndex)
+ {
+ if (array == null)
+ throw new ArgumentNullException("array");
+ if (array.Length < LineCount)
+ throw new ArgumentException("The array is too small", "array");
+ if (arrayIndex < 0 || arrayIndex + LineCount > array.Length)
+ throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, "Value must be between 0 and " + (array.Length - LineCount));
+ foreach (DocumentLine ls in this) {
+ array[arrayIndex++] = ls;
+ }
+ }
+
+ bool ICollection<DocumentLine>.Remove(DocumentLine item)
+ {
+ throw new NotSupportedException();
+ }
+
+ public IEnumerator<DocumentLine> GetEnumerator()
+ {
+ document.VerifyAccess();
+ return Enumerate();
+ }
+
+ IEnumerator<DocumentLine> Enumerate()
+ {
+ document.VerifyAccess();
+ DocumentLine line = root.LeftMost;
+ while (line != null) {
+ yield return line;
+ line = line.NextLine;
+ }
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+ #endregion
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/GapTextBuffer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/GapTextBuffer.cs
new file mode 100644
index 000000000..a617284a9
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/GapTextBuffer.cs
@@ -0,0 +1,192 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Diagnostics;
+using System.Text;
+
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /*
+ /// <summary>
+ /// Implementation of a gap text buffer.
+ /// </summary>
+ sealed class GapTextBuffer
+ {
+ char[] buffer = Empty<char>.Array;
+
+ /// <summary>
+ /// The current text content.
+ /// Is set to null whenever the buffer changes, and gets a value only when the
+ /// full text content is requested.
+ /// </summary>
+ string textContent;
+
+ /// <summary>
+ /// last GetText result
+ /// </summary>
+ string lastGetTextResult;
+ int lastGetTextRequestOffset;
+
+ int gapBeginOffset;
+ int gapEndOffset;
+ int gapLength; // gapLength == gapEndOffset - gapBeginOffset
+
+ /// <summary>
+ /// when gap is too small for inserted text or gap is too large (exceeds maxGapLength),
+ /// a new buffer is reallocated with a new gap of at least this size.
+ /// </summary>
+ const int minGapLength = 128;
+
+ /// <summary>
+ /// when the gap exceeds this size, reallocate a smaller buffer
+ /// </summary>
+ const int maxGapLength = 4096;
+
+ public int Length {
+ get {
+ return buffer.Length - gapLength;
+ }
+ }
+
+ /// <summary>
+ /// Gets the buffer content.
+ /// </summary>
+ public string Text {
+ get {
+ if (textContent == null)
+ textContent = GetText(0, Length);
+ return textContent;
+ }
+ set {
+ Debug.Assert(value != null);
+ textContent = value; lastGetTextResult = null;
+ buffer = new char[value.Length + minGapLength];
+ value.CopyTo(0, buffer, 0, value.Length);
+ gapBeginOffset = value.Length;
+ gapEndOffset = buffer.Length;
+ gapLength = gapEndOffset - gapBeginOffset;
+ }
+ }
+
+ public char GetCharAt(int offset)
+ {
+ return offset < gapBeginOffset ? buffer[offset] : buffer[offset + gapLength];
+ }
+
+ public string GetText(int offset, int length)
+ {
+ if (length == 0)
+ return string.Empty;
+ if (lastGetTextRequestOffset == offset && lastGetTextResult != null && length == lastGetTextResult.Length)
+ return lastGetTextResult;
+
+ int end = offset + length;
+ string result;
+ if (end < gapBeginOffset) {
+ result = new string(buffer, offset, length);
+ } else if (offset > gapBeginOffset) {
+ result = new string(buffer, offset + gapLength, length);
+ } else {
+ int block1Size = gapBeginOffset - offset;
+ int block2Size = end - gapBeginOffset;
+
+ StringBuilder buf = new StringBuilder(block1Size + block2Size);
+ buf.Append(buffer, offset, block1Size);
+ buf.Append(buffer, gapEndOffset, block2Size);
+ result = buf.ToString();
+ }
+ lastGetTextRequestOffset = offset;
+ lastGetTextResult = result;
+ return result;
+ }
+
+ /// <summary>
+ /// Inserts text at the specified offset.
+ /// </summary>
+ public void Insert(int offset, string text)
+ {
+ Debug.Assert(offset >= 0 && offset <= Length);
+
+ if (text.Length == 0)
+ return;
+
+ textContent = null; lastGetTextResult = null;
+ PlaceGap(offset, text.Length);
+ text.CopyTo(0, buffer, gapBeginOffset, text.Length);
+ gapBeginOffset += text.Length;
+ gapLength = gapEndOffset - gapBeginOffset;
+ }
+
+ /// <summary>
+ /// Remove <paramref name="length"/> characters at <paramref name="offset"/>.
+ /// Leave a gap of at least <paramref name="reserveGapSize"/>.
+ /// </summary>
+ public void Remove(int offset, int length, int reserveGapSize)
+ {
+ Debug.Assert(offset >= 0 && offset <= Length);
+ Debug.Assert(length >= 0 && offset + length <= Length);
+ Debug.Assert(reserveGapSize >= 0);
+
+ if (length == 0)
+ return;
+
+ textContent = null; lastGetTextResult = null;
+ PlaceGap(offset, reserveGapSize - length);
+ gapEndOffset += length; // delete removed text
+ gapLength = gapEndOffset - gapBeginOffset;
+ if (gapLength - reserveGapSize > maxGapLength && gapLength - reserveGapSize > buffer.Length / 4) {
+ // shrink gap
+ MakeNewBuffer(gapBeginOffset, reserveGapSize + minGapLength);
+ }
+ }
+
+ void PlaceGap(int newGapOffset, int minRequiredGapLength)
+ {
+ if (gapLength < minRequiredGapLength) {
+ // enlarge gap
+ MakeNewBuffer(newGapOffset, minRequiredGapLength + Math.Max(minGapLength, buffer.Length / 8));
+ } else {
+ while (newGapOffset < gapBeginOffset) {
+ buffer[--gapEndOffset] = buffer[--gapBeginOffset];
+ }
+ while (newGapOffset > gapBeginOffset) {
+ buffer[gapBeginOffset++] = buffer[gapEndOffset++];
+ }
+ }
+ }
+
+ void MakeNewBuffer(int newGapOffset, int newGapLength)
+ {
+ char[] newBuffer = new char[Length + newGapLength];
+ Debug.WriteLine("GapTextBuffer was reallocated, new size=" + newBuffer.Length);
+ if (newGapOffset < gapBeginOffset) {
+ // gap is moving backwards
+
+ // first part:
+ Array.Copy(buffer, 0, newBuffer, 0, newGapOffset);
+ // moving middle part:
+ Array.Copy(buffer, newGapOffset, newBuffer, newGapOffset + newGapLength, gapBeginOffset - newGapOffset);
+ // last part:
+ Array.Copy(buffer, gapEndOffset, newBuffer, newBuffer.Length - (buffer.Length - gapEndOffset), buffer.Length - gapEndOffset);
+ } else {
+ // gap is moving forwards
+ // first part:
+ Array.Copy(buffer, 0, newBuffer, 0, gapBeginOffset);
+ // moving middle part:
+ Array.Copy(buffer, gapEndOffset, newBuffer, gapBeginOffset, newGapOffset - gapBeginOffset);
+ // last part:
+ int lastPartLength = newBuffer.Length - (newGapOffset + newGapLength);
+ Array.Copy(buffer, buffer.Length - lastPartLength, newBuffer, newGapOffset + newGapLength, lastPartLength);
+ }
+
+ gapBeginOffset = newGapOffset;
+ gapEndOffset = newGapOffset + newGapLength;
+ gapLength = newGapLength;
+ buffer = newBuffer;
+ }
+ }
+ */
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ILineTracker.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ILineTracker.cs
new file mode 100644
index 000000000..c16128d63
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ILineTracker.cs
@@ -0,0 +1,55 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Allows for low-level line tracking.
+ /// </summary>
+ /// <remarks>
+ /// The methods on this interface are called by the TextDocument's LineManager immediately after the document
+ /// has changed, *while* the DocumentLineTree is updating.
+ /// Thus, the DocumentLineTree may be in an invalid state when these methods are called.
+ /// This interface should only be used to update per-line data structures like the HeightTree.
+ /// Line trackers must not cause any events to be raised during an update to prevent other code from seeing
+ /// the invalid state.
+ /// Line trackers may be called while the TextDocument has taken a lock.
+ /// You must be careful not to dead-lock inside ILineTracker callbacks.
+ /// </remarks>
+ public interface ILineTracker
+ {
+ /// <summary>
+ /// Is called immediately before a document line is removed.
+ /// </summary>
+ void BeforeRemoveLine(DocumentLine line);
+
+// /// <summary>
+// /// Is called immediately after a document line is removed.
+// /// </summary>
+// void AfterRemoveLine(DocumentLine line);
+
+ /// <summary>
+ /// Is called immediately before a document line changes length.
+ /// This method will be called whenever the line is changed, even when the length stays as it is.
+ /// The method might be called multiple times for a single line because
+ /// a replacement is internally handled as removal followed by insertion.
+ /// </summary>
+ void SetLineLength(DocumentLine line, int newTotalLength);
+
+ /// <summary>
+ /// Is called immediately after a line was inserted.
+ /// </summary>
+ /// <param name="newLine">The new line</param>
+ /// <param name="insertionPos">The existing line before the new line</param>
+ void LineInserted(DocumentLine insertionPos, DocumentLine newLine);
+
+ /// <summary>
+ /// Indicates that there were changes to the document that the line tracker was not notified of.
+ /// The document is in a consistent state (but the line trackers aren't), and line trackers should
+ /// throw away their data and rebuild the document.
+ /// </summary>
+ void RebuildDocument();
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ISegment.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ISegment.cs
new file mode 100644
index 000000000..80eb63352
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ISegment.cs
@@ -0,0 +1,219 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Diagnostics;
+using Tango.Scripting.Editors.Utils;
+using System.Globalization;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// An (Offset,Length)-pair.
+ /// </summary>
+ /// <seealso cref="TextSegment"/>
+ /// <seealso cref="AnchorSegment"/>
+ public interface ISegment
+ {
+ /// <summary>
+ /// Gets the start offset of the segment.
+ /// </summary>
+ int Offset { get; }
+
+ /// <summary>
+ /// Gets the length of the segment.
+ /// </summary>
+ /// <remarks>Must not be negative.</remarks>
+ int Length { get; }
+
+ /// <summary>
+ /// Gets the end offset of the segment.
+ /// </summary>
+ /// <remarks>EndOffset = Offset + Length;</remarks>
+ int EndOffset { get; }
+ }
+
+ static class SegmentExtensions
+ {
+ /// <summary>
+ /// Gets whether the segment contains the offset.
+ /// </summary>
+ /// <returns>
+ /// True, if offset is between segment.Start and segment.End (inclusive); otherwise, false.
+ /// </returns>
+ public static bool Contains(this ISegment segment, int offset)
+ {
+ int start = segment.Offset;
+ int end = start + segment.Length;
+ return offset >= start && offset <= end;
+ }
+
+ /// <summary>
+ /// Gets the overlapping portion of the segments.
+ /// Returns SimpleSegment.Invalid if the segments don't overlap.
+ /// </summary>
+ public static SimpleSegment GetOverlap(this ISegment segment, ISegment other)
+ {
+ int start = Math.Max(segment.Offset, other.Offset);
+ int end = Math.Min(segment.EndOffset, other.EndOffset);
+ if (end < start)
+ return SimpleSegment.Invalid;
+ else
+ return new SimpleSegment(start, end - start);
+ }
+ }
+
+ /// <summary>
+ /// Represents a simple segment (Offset,Length pair) that is not automatically updated
+ /// on document changes.
+ /// </summary>
+ struct SimpleSegment : IEquatable<SimpleSegment>, ISegment
+ {
+ public static readonly SimpleSegment Invalid = new SimpleSegment(-1, -1);
+
+ public readonly int Offset, Length;
+
+ int ISegment.Offset {
+ get { return Offset; }
+ }
+
+ int ISegment.Length {
+ get { return Length; }
+ }
+
+ public int EndOffset {
+ get {
+ return Offset + Length;
+ }
+ }
+
+ public SimpleSegment(int offset, int length)
+ {
+ this.Offset = offset;
+ this.Length = length;
+ }
+
+ public SimpleSegment(ISegment segment)
+ {
+ Debug.Assert(segment != null);
+ this.Offset = segment.Offset;
+ this.Length = segment.Length;
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked {
+ return Offset + 10301 * Length;
+ }
+ }
+
+ public override bool Equals(object obj)
+ {
+ return (obj is SimpleSegment) && Equals((SimpleSegment)obj);
+ }
+
+ public bool Equals(SimpleSegment other)
+ {
+ return this.Offset == other.Offset && this.Length == other.Length;
+ }
+
+ public static bool operator ==(SimpleSegment left, SimpleSegment right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(SimpleSegment left, SimpleSegment right)
+ {
+ return !left.Equals(right);
+ }
+
+ public override string ToString()
+ {
+ return "[Offset=" + Offset.ToString(CultureInfo.InvariantCulture) + ", Length=" + Length.ToString(CultureInfo.InvariantCulture) + "]";
+ }
+ }
+
+ /// <summary>
+ /// A segment using <see cref="TextAnchor"/>s as start and end positions.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// For the constructors creating new anchors, the start position will be AfterInsertion and the end position will be BeforeInsertion.
+ /// Should the end position move before the start position, the segment will have length 0.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="ISegment"/>
+ /// <seealso cref="TextSegment"/>
+ public sealed class AnchorSegment : ISegment
+ {
+ readonly TextAnchor start, end;
+
+ /// <inheritdoc/>
+ public int Offset {
+ get { return start.Offset; }
+ }
+
+ /// <inheritdoc/>
+ public int Length {
+ get {
+ // Math.Max takes care of the fact that end.Offset might move before start.Offset.
+ return Math.Max(0, end.Offset - start.Offset);
+ }
+ }
+
+ /// <inheritdoc/>
+ public int EndOffset {
+ get {
+ // Math.Max takes care of the fact that end.Offset might move before start.Offset.
+ return Math.Max(start.Offset, end.Offset);
+ }
+ }
+
+ /// <summary>
+ /// Creates a new AnchorSegment using the specified anchors.
+ /// The anchors must have <see cref="TextAnchor.SurviveDeletion"/> set to true.
+ /// </summary>
+ public AnchorSegment(TextAnchor start, TextAnchor end)
+ {
+ if (start == null)
+ throw new ArgumentNullException("start");
+ if (end == null)
+ throw new ArgumentNullException("end");
+ if (!start.SurviveDeletion)
+ throw new ArgumentException("Anchors for AnchorSegment must use SurviveDeletion", "start");
+ if (!end.SurviveDeletion)
+ throw new ArgumentException("Anchors for AnchorSegment must use SurviveDeletion", "end");
+ this.start = start;
+ this.end = end;
+ }
+
+ /// <summary>
+ /// Creates a new AnchorSegment that creates new anchors.
+ /// </summary>
+ public AnchorSegment(TextDocument document, ISegment segment)
+ : this(document, ThrowUtil.CheckNotNull(segment, "segment").Offset, segment.Length)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new AnchorSegment that creates new anchors.
+ /// </summary>
+ public AnchorSegment(TextDocument document, int offset, int length)
+ {
+ if (document == null)
+ throw new ArgumentNullException("document");
+ this.start = document.CreateAnchor(offset);
+ this.start.SurviveDeletion = true;
+ this.start.MovementType = AnchorMovementType.AfterInsertion;
+ this.end = document.CreateAnchor(offset + length);
+ this.end.SurviveDeletion = true;
+ this.end.MovementType = AnchorMovementType.BeforeInsertion;
+ }
+
+ /// <inheritdoc/>
+ public override string ToString()
+ {
+ return "[Offset=" + Offset.ToString(CultureInfo.InvariantCulture) + ", EndOffset=" + EndOffset.ToString(CultureInfo.InvariantCulture) + "]";
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ITextSource.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ITextSource.cs
new file mode 100644
index 000000000..c52a333c5
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/ITextSource.cs
@@ -0,0 +1,320 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using Tango.Scripting.Editors.Utils;
+using System;
+using System.IO;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Interface for read-only access to a text source.
+ /// </summary>
+ /// <seealso cref="TextDocument"/>
+ /// <seealso cref="StringTextSource"/>
+ public interface ITextSource
+ {
+ /// <summary>
+ /// Gets the whole text as string.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")]
+ string Text { get; }
+
+ /// <summary>
+ /// Is raised when the Text property changes.
+ /// </summary>
+ event EventHandler TextChanged;
+
+ /// <summary>
+ /// Gets the total text length.
+ /// </summary>
+ /// <returns>The length of the text, in characters.</returns>
+ /// <remarks>This is the same as Text.Length, but is more efficient because
+ /// it doesn't require creating a String object.</remarks>
+ int TextLength { get; }
+
+ /// <summary>
+ /// Gets a character at the specified position in the document.
+ /// </summary>
+ /// <paramref name="offset">The index of the character to get.</paramref>
+ /// <exception cref="ArgumentOutOfRangeException">Offset is outside the valid range (0 to TextLength-1).</exception>
+ /// <returns>The character at the specified position.</returns>
+ /// <remarks>This is the same as Text[offset], but is more efficient because
+ /// it doesn't require creating a String object.</remarks>
+ char GetCharAt(int offset);
+
+ /// <summary>
+ /// Gets the index of the first occurrence of any character in the specified array.
+ /// </summary>
+ /// <param name="anyOf"></param>
+ /// <param name="startIndex">Start index of the search.</param>
+ /// <param name="count">Length of the area to search.</param>
+ /// <returns>The first index where any character was found; or -1 if no occurrence was found.</returns>
+ int IndexOfAny(char[] anyOf, int startIndex, int count);
+
+ /// <summary>
+ /// Retrieves the text for a portion of the document.
+ /// </summary>
+ /// <exception cref="ArgumentOutOfRangeException">offset or length is outside the valid range.</exception>
+ /// <remarks>This is the same as Text.Substring, but is more efficient because
+ /// it doesn't require creating a String object for the whole document.</remarks>
+ string GetText(int offset, int length);
+
+ /// <summary>
+ /// Creates a snapshot of the current text.
+ /// </summary>
+ /// <remarks>
+ /// This method is generally not thread-safe when called on a mutable text buffer, but the resulting text buffer is immutable and thread-safe.
+ /// However, some implementing classes may provide additional thread-safety guarantees, see <see cref="TextDocument.CreateSnapshot()">TextDocument.CreateSnapshot</see>.
+ /// </remarks>
+ ITextSource CreateSnapshot();
+
+ /// <summary>
+ /// Creates a snapshot of a part of the current text.
+ /// </summary>
+ /// <remarks>
+ /// This method is generally not thread-safe when called on a mutable text buffer, but the resulting text buffer is immutable and thread-safe.
+ /// However, some implementing classes may provide additional thread-safety guarantees, see <see cref="TextDocument.CreateSnapshot()">TextDocument.CreateSnapshot</see>.
+ /// </remarks>
+ ITextSource CreateSnapshot(int offset, int length);
+
+ /// <summary>
+ /// Creates a text reader.
+ /// If the text is changed while a reader is active, the reader will continue to read from the old text version.
+ /// </summary>
+ TextReader CreateReader();
+ }
+
+ /// <summary>
+ /// Implements the ITextSource interface by wrapping another TextSource
+ /// and viewing only a part of the text.
+ /// </summary>
+ [Obsolete("This class will be removed in a future version of AvalonEdit")]
+ public sealed class TextSourceView : ITextSource
+ {
+ readonly ITextSource baseTextSource;
+ readonly ISegment viewedSegment;
+
+ /// <summary>
+ /// Creates a new TextSourceView object.
+ /// </summary>
+ /// <param name="baseTextSource">The base text source.</param>
+ /// <param name="viewedSegment">A text segment from the base text source</param>
+ public TextSourceView(ITextSource baseTextSource, ISegment viewedSegment)
+ {
+ if (baseTextSource == null)
+ throw new ArgumentNullException("baseTextSource");
+ if (viewedSegment == null)
+ throw new ArgumentNullException("viewedSegment");
+ this.baseTextSource = baseTextSource;
+ this.viewedSegment = viewedSegment;
+ }
+
+ /// <inheritdoc/>
+ public event EventHandler TextChanged {
+ add { baseTextSource.TextChanged += value; }
+ remove { baseTextSource.TextChanged -= value; }
+ }
+
+ /// <inheritdoc/>
+ public string Text {
+ get {
+ return baseTextSource.GetText(viewedSegment.Offset, viewedSegment.Length);
+ }
+ }
+
+ /// <inheritdoc/>
+ public int TextLength {
+ get { return viewedSegment.Length; }
+ }
+
+ /// <inheritdoc/>
+ public char GetCharAt(int offset)
+ {
+ return baseTextSource.GetCharAt(viewedSegment.Offset + offset);
+ }
+
+ /// <inheritdoc/>
+ public string GetText(int offset, int length)
+ {
+ return baseTextSource.GetText(viewedSegment.Offset + offset, length);
+ }
+
+ /// <inheritdoc/>
+ public ITextSource CreateSnapshot()
+ {
+ return baseTextSource.CreateSnapshot(viewedSegment.Offset, viewedSegment.Length);
+ }
+
+ /// <inheritdoc/>
+ public ITextSource CreateSnapshot(int offset, int length)
+ {
+ return baseTextSource.CreateSnapshot(viewedSegment.Offset + offset, length);
+ }
+
+ /// <inheritdoc/>
+ public TextReader CreateReader()
+ {
+ return CreateSnapshot().CreateReader();
+ }
+
+ /// <inheritdoc/>
+ public int IndexOfAny(char[] anyOf, int startIndex, int count)
+ {
+ int offset = viewedSegment.Offset;
+ int result = baseTextSource.IndexOfAny(anyOf, startIndex + offset, count);
+ return result >= 0 ? result - offset : result;
+ }
+ }
+
+ /// <summary>
+ /// Implements the ITextSource interface using a string.
+ /// </summary>
+ public sealed class StringTextSource : ITextSource
+ {
+ readonly string text;
+
+ /// <summary>
+ /// Creates a new StringTextSource.
+ /// </summary>
+ public StringTextSource(string text)
+ {
+ if (text == null)
+ throw new ArgumentNullException("text");
+ this.text = text;
+ }
+
+ // Text can never change
+ event EventHandler ITextSource.TextChanged { add {} remove {} }
+
+ /// <inheritdoc/>
+ public string Text {
+ get { return text; }
+ }
+
+ /// <inheritdoc/>
+ public int TextLength {
+ get { return text.Length; }
+ }
+
+ /// <inheritdoc/>
+ public char GetCharAt(int offset)
+ {
+ // GetCharAt must throw ArgumentOutOfRangeException, not IndexOutOfRangeException
+ if (offset < 0 || offset >= text.Length)
+ throw new ArgumentOutOfRangeException("offset", offset, "offset must be between 0 and " + (text.Length - 1));
+ return text[offset];
+ }
+
+ /// <inheritdoc/>
+ public string GetText(int offset, int length)
+ {
+ return text.Substring(offset, length);
+ }
+
+ /// <inheritdoc/>
+ public TextReader CreateReader()
+ {
+ return new StringReader(text);
+ }
+
+ /// <inheritdoc/>
+ public ITextSource CreateSnapshot()
+ {
+ return this; // StringTextSource already is immutable
+ }
+
+ /// <inheritdoc/>
+ public ITextSource CreateSnapshot(int offset, int length)
+ {
+ return new StringTextSource(text.Substring(offset, length));
+ }
+
+ /// <inheritdoc/>
+ public int IndexOfAny(char[] anyOf, int startIndex, int count)
+ {
+ return text.IndexOfAny(anyOf, startIndex, count);
+ }
+ }
+
+ /// <summary>
+ /// Implements the ITextSource interface using a rope.
+ /// </summary>
+ public sealed class RopeTextSource : ITextSource
+ {
+ readonly Rope<char> rope;
+
+ /// <summary>
+ /// Creates a new RopeTextSource.
+ /// </summary>
+ public RopeTextSource(Rope<char> rope)
+ {
+ if (rope == null)
+ throw new ArgumentNullException("rope");
+ this.rope = rope;
+ }
+
+ /// <summary>
+ /// Returns a clone of the rope used for this text source.
+ /// </summary>
+ /// <remarks>
+ /// RopeTextSource only publishes a copy of the contained rope to ensure that the underlying rope cannot be modified.
+ /// Unless the creator of the RopeTextSource still has a reference on the rope, RopeTextSource is immutable.
+ /// </remarks>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification="Not a property because it creates a clone")]
+ public Rope<char> GetRope()
+ {
+ return rope.Clone();
+ }
+
+ // Change event is not supported
+ event EventHandler ITextSource.TextChanged { add {} remove {} }
+
+ /// <inheritdoc/>
+ public string Text {
+ get { return rope.ToString(); }
+ }
+
+ /// <inheritdoc/>
+ public int TextLength {
+ get { return rope.Length; }
+ }
+
+ /// <inheritdoc/>
+ public char GetCharAt(int offset)
+ {
+ return rope[offset];
+ }
+
+ /// <inheritdoc/>
+ public string GetText(int offset, int length)
+ {
+ return rope.ToString(offset, length);
+ }
+
+ /// <inheritdoc/>
+ public TextReader CreateReader()
+ {
+ return new RopeTextReader(rope);
+ }
+
+ /// <inheritdoc/>
+ public ITextSource CreateSnapshot()
+ {
+ // we clone the underlying rope because the creator of the RopeTextSource might be modifying it
+ return new RopeTextSource(rope.Clone());
+ }
+
+ /// <inheritdoc/>
+ public ITextSource CreateSnapshot(int offset, int length)
+ {
+ return new RopeTextSource(rope.GetRange(offset, length));
+ }
+
+ /// <inheritdoc/>
+ public int IndexOfAny(char[] anyOf, int startIndex, int count)
+ {
+ return rope.IndexOfAny(anyOf, startIndex, count);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/IUndoableOperation.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/IUndoableOperation.cs
new file mode 100644
index 000000000..2e19c462c
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/IUndoableOperation.cs
@@ -0,0 +1,30 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// This Interface describes a the basic Undo/Redo operation
+ /// all Undo Operations must implement this interface.
+ /// </summary>
+ public interface IUndoableOperation
+ {
+ /// <summary>
+ /// Undo the last operation
+ /// </summary>
+ void Undo();
+
+ /// <summary>
+ /// Redo the last operation
+ /// </summary>
+ void Redo();
+ }
+
+ interface IUndoableOperationWithContext : IUndoableOperation
+ {
+ void Undo(UndoStack stack);
+ void Redo(UndoStack stack);
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/LineManager.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/LineManager.cs
new file mode 100644
index 000000000..ecab1fa48
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/LineManager.cs
@@ -0,0 +1,288 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using Tango.Scripting.Editors.Utils;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Creates/Deletes lines when text is inserted/removed.
+ /// </summary>
+ sealed class LineManager
+ {
+ #region Constructor
+ readonly TextDocument document;
+ readonly DocumentLineTree documentLineTree;
+
+ /// <summary>
+ /// A copy of the line trackers. We need a copy so that line trackers may remove themselves
+ /// while being notified (used e.g. by WeakLineTracker)
+ /// </summary>
+ ILineTracker[] lineTrackers;
+
+ internal void UpdateListOfLineTrackers()
+ {
+ this.lineTrackers = document.LineTrackers.ToArray();
+ }
+
+ public LineManager(DocumentLineTree documentLineTree, TextDocument document)
+ {
+ this.document = document;
+ this.documentLineTree = documentLineTree;
+ UpdateListOfLineTrackers();
+
+ Rebuild();
+ }
+ #endregion
+
+ #region Change events
+ /*
+ HashSet<DocumentLine> deletedLines = new HashSet<DocumentLine>();
+ readonly HashSet<DocumentLine> changedLines = new HashSet<DocumentLine>();
+ HashSet<DocumentLine> deletedOrChangedLines = new HashSet<DocumentLine>();
+
+ /// <summary>
+ /// Gets the list of lines deleted since the last RetrieveChangedLines() call.
+ /// The returned list is unsorted.
+ /// </summary>
+ public ICollection<DocumentLine> RetrieveDeletedLines()
+ {
+ var r = deletedLines;
+ deletedLines = new HashSet<DocumentLine>();
+ return r;
+ }
+
+ /// <summary>
+ /// Gets the list of lines changed since the last RetrieveChangedLines() call.
+ /// The returned list is sorted by line number and does not contain deleted lines.
+ /// </summary>
+ public List<DocumentLine> RetrieveChangedLines()
+ {
+ var list = (from line in changedLines
+ where !line.IsDeleted
+ let number = line.LineNumber
+ orderby number
+ select line).ToList();
+ changedLines.Clear();
+ return list;
+ }
+
+ /// <summary>
+ /// Gets the list of lines changed since the last RetrieveDeletedOrChangedLines() call.
+ /// The returned list is not sorted.
+ /// </summary>
+ public ICollection<DocumentLine> RetrieveDeletedOrChangedLines()
+ {
+ var r = deletedOrChangedLines;
+ deletedOrChangedLines = new HashSet<DocumentLine>();
+ return r;
+ }
+ */
+ #endregion
+
+ #region Rebuild
+ public void Rebuild()
+ {
+ // keep the first document line
+ DocumentLine ls = documentLineTree.GetByNumber(1);
+ SimpleSegment ds = NewLineFinder.NextNewLine(document, 0);
+ List<DocumentLine> lines = new List<DocumentLine>();
+ int lastDelimiterEnd = 0;
+ while (ds != SimpleSegment.Invalid) {
+ ls.TotalLength = ds.Offset + ds.Length - lastDelimiterEnd;
+ ls.DelimiterLength = ds.Length;
+ lastDelimiterEnd = ds.Offset + ds.Length;
+ lines.Add(ls);
+
+ ls = new DocumentLine(document);
+ ds = NewLineFinder.NextNewLine(document, lastDelimiterEnd);
+ }
+ ls.ResetLine();
+ ls.TotalLength = document.TextLength - lastDelimiterEnd;
+ lines.Add(ls);
+ documentLineTree.RebuildTree(lines);
+ foreach (ILineTracker lineTracker in lineTrackers)
+ lineTracker.RebuildDocument();
+ }
+ #endregion
+
+ #region Remove
+ public void Remove(int offset, int length)
+ {
+ Debug.Assert(length >= 0);
+ if (length == 0) return;
+ DocumentLine startLine = documentLineTree.GetByOffset(offset);
+ int startLineOffset = startLine.Offset;
+
+ Debug.Assert(offset < startLineOffset + startLine.TotalLength);
+ if (offset > startLineOffset + startLine.Length) {
+ Debug.Assert(startLine.DelimiterLength == 2);
+ // we are deleting starting in the middle of a delimiter
+
+ // remove last delimiter part
+ SetLineLength(startLine, startLine.TotalLength - 1);
+ // remove remaining text
+ Remove(offset, length - 1);
+ return;
+ }
+
+ if (offset + length < startLineOffset + startLine.TotalLength) {
+ // just removing a part of this line
+ //startLine.RemovedLinePart(ref deferredEventList, offset - startLineOffset, length);
+ SetLineLength(startLine, startLine.TotalLength - length);
+ return;
+ }
+ // merge startLine with another line because startLine's delimiter was deleted
+ // possibly remove lines in between if multiple delimiters were deleted
+ int charactersRemovedInStartLine = startLineOffset + startLine.TotalLength - offset;
+ Debug.Assert(charactersRemovedInStartLine > 0);
+ //startLine.RemovedLinePart(ref deferredEventList, offset - startLineOffset, charactersRemovedInStartLine);
+
+
+ DocumentLine endLine = documentLineTree.GetByOffset(offset + length);
+ if (endLine == startLine) {
+ // special case: we are removing a part of the last line up to the
+ // end of the document
+ SetLineLength(startLine, startLine.TotalLength - length);
+ return;
+ }
+ int endLineOffset = endLine.Offset;
+ int charactersLeftInEndLine = endLineOffset + endLine.TotalLength - (offset + length);
+ //endLine.RemovedLinePart(ref deferredEventList, 0, endLine.TotalLength - charactersLeftInEndLine);
+ //startLine.MergedWith(endLine, offset - startLineOffset);
+
+ // remove all lines between startLine (excl.) and endLine (incl.)
+ DocumentLine tmp = startLine.NextLine;
+ DocumentLine lineToRemove;
+ do {
+ lineToRemove = tmp;
+ tmp = tmp.NextLine;
+ RemoveLine(lineToRemove);
+ } while (lineToRemove != endLine);
+
+ SetLineLength(startLine, startLine.TotalLength - charactersRemovedInStartLine + charactersLeftInEndLine);
+ }
+
+ void RemoveLine(DocumentLine lineToRemove)
+ {
+ foreach (ILineTracker lt in lineTrackers)
+ lt.BeforeRemoveLine(lineToRemove);
+ documentLineTree.RemoveLine(lineToRemove);
+// foreach (ILineTracker lt in lineTracker)
+// lt.AfterRemoveLine(lineToRemove);
+// deletedLines.Add(lineToRemove);
+// deletedOrChangedLines.Add(lineToRemove);
+ }
+
+ #endregion
+
+ #region Insert
+ public void Insert(int offset, string text)
+ {
+ DocumentLine line = documentLineTree.GetByOffset(offset);
+ int lineOffset = line.Offset;
+
+ Debug.Assert(offset <= lineOffset + line.TotalLength);
+ if (offset > lineOffset + line.Length) {
+ Debug.Assert(line.DelimiterLength == 2);
+ // we are inserting in the middle of a delimiter
+
+ // shorten line
+ SetLineLength(line, line.TotalLength - 1);
+ // add new line
+ line = InsertLineAfter(line, 1);
+ line = SetLineLength(line, 1);
+ }
+
+ SimpleSegment ds = NewLineFinder.NextNewLine(text, 0);
+ if (ds == SimpleSegment.Invalid) {
+ // no newline is being inserted, all text is inserted in a single line
+ //line.InsertedLinePart(offset - line.Offset, text.Length);
+ SetLineLength(line, line.TotalLength + text.Length);
+ return;
+ }
+ //DocumentLine firstLine = line;
+ //firstLine.InsertedLinePart(offset - firstLine.Offset, ds.Offset);
+ int lastDelimiterEnd = 0;
+ while (ds != SimpleSegment.Invalid) {
+ // split line segment at line delimiter
+ int lineBreakOffset = offset + ds.Offset + ds.Length;
+ lineOffset = line.Offset;
+ int lengthAfterInsertionPos = lineOffset + line.TotalLength - (offset + lastDelimiterEnd);
+ line = SetLineLength(line, lineBreakOffset - lineOffset);
+ DocumentLine newLine = InsertLineAfter(line, lengthAfterInsertionPos);
+ newLine = SetLineLength(newLine, lengthAfterInsertionPos);
+
+ line = newLine;
+ lastDelimiterEnd = ds.Offset + ds.Length;
+
+ ds = NewLineFinder.NextNewLine(text, lastDelimiterEnd);
+ }
+ //firstLine.SplitTo(line);
+ // insert rest after last delimiter
+ if (lastDelimiterEnd != text.Length) {
+ //line.InsertedLinePart(0, text.Length - lastDelimiterEnd);
+ SetLineLength(line, line.TotalLength + text.Length - lastDelimiterEnd);
+ }
+ }
+
+ DocumentLine InsertLineAfter(DocumentLine line, int length)
+ {
+ DocumentLine newLine = documentLineTree.InsertLineAfter(line, length);
+ foreach (ILineTracker lt in lineTrackers)
+ lt.LineInserted(line, newLine);
+ return newLine;
+ }
+ #endregion
+
+ #region SetLineLength
+ /// <summary>
+ /// Sets the total line length and checks the delimiter.
+ /// This method can cause line to be deleted when it contains a single '\n' character
+ /// and the previous line ends with '\r'.
+ /// </summary>
+ /// <returns>Usually returns <paramref name="line"/>, but if line was deleted due to
+ /// the "\r\n" merge, returns the previous line.</returns>
+ DocumentLine SetLineLength(DocumentLine line, int newTotalLength)
+ {
+// changedLines.Add(line);
+// deletedOrChangedLines.Add(line);
+ int delta = newTotalLength - line.TotalLength;
+ if (delta != 0) {
+ foreach (ILineTracker lt in lineTrackers)
+ lt.SetLineLength(line, newTotalLength);
+ line.TotalLength = newTotalLength;
+ DocumentLineTree.UpdateAfterChildrenChange(line);
+ }
+ // determine new DelimiterLength
+ if (newTotalLength == 0) {
+ line.DelimiterLength = 0;
+ } else {
+ int lineOffset = line.Offset;
+ char lastChar = document.GetCharAt(lineOffset + newTotalLength - 1);
+ if (lastChar == '\r') {
+ line.DelimiterLength = 1;
+ } else if (lastChar == '\n') {
+ if (newTotalLength >= 2 && document.GetCharAt(lineOffset + newTotalLength - 2) == '\r') {
+ line.DelimiterLength = 2;
+ } else if (newTotalLength == 1 && lineOffset > 0 && document.GetCharAt(lineOffset - 1) == '\r') {
+ // we need to join this line with the previous line
+ DocumentLine previousLine = line.PreviousLine;
+ RemoveLine(line);
+ return SetLineLength(previousLine, previousLine.TotalLength + 1);
+ } else {
+ line.DelimiterLength = 1;
+ }
+ } else {
+ line.DelimiterLength = 0;
+ }
+ }
+ return line;
+ }
+ #endregion
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/LineNode.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/LineNode.cs
new file mode 100644
index 000000000..317202721
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/LineNode.cs
@@ -0,0 +1,83 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+
+namespace Tango.Scripting.Editors.Document
+{
+ using LineNode = DocumentLine;
+
+ // A tree node in the document line tree.
+ // For the purpose of the invariants, "children", "descendents", "siblings" etc. include the DocumentLine object,
+ // it is treated as a third child node between left and right.
+
+ // Originally, this was a separate class, with a reference to the documentLine. The documentLine had a reference
+ // back to the node. To save memory, the same object is used for both the documentLine and the line node.
+ // This saves 16 bytes per line (8 byte object overhead + two pointers).
+// sealed class LineNode
+// {
+// internal readonly DocumentLine documentLine;
+ partial class DocumentLine
+ {
+ internal DocumentLine left, right, parent;
+ internal bool color;
+ // optimization note: I tried packing color and isDeleted into a single byte field, but that
+ // actually increased the memory requirements. The JIT packs two bools and a byte (delimiterSize)
+ // into a single DWORD, but two bytes get each their own DWORD. Three bytes end up in the same DWORD, so
+ // apparently the JIT only optimizes for memory when there are at least three small fields.
+ // Currently, DocumentLine takes 36 bytes on x86 (8 byte object overhead, 3 pointers, 3 ints, and another DWORD
+ // for the small fields).
+ // TODO: a possible optimization would be to combine 'totalLength' and the small fields into a single uint.
+ // delimiterSize takes only two bits, the two bools take another two bits; so there's still
+ // 28 bits left for totalLength. 268435455 characters per line should be enough for everyone :)
+
+ /// <summary>
+ /// Resets the line to enable its reuse after a document rebuild.
+ /// </summary>
+ internal void ResetLine()
+ {
+ totalLength = delimiterLength = 0;
+ isDeleted = color = false;
+ left = right = parent = null;
+ }
+
+ internal LineNode InitLineNode()
+ {
+ this.nodeTotalCount = 1;
+ this.nodeTotalLength = this.TotalLength;
+ return this;
+ }
+
+ internal LineNode LeftMost {
+ get {
+ LineNode node = this;
+ while (node.left != null)
+ node = node.left;
+ return node;
+ }
+ }
+
+ internal LineNode RightMost {
+ get {
+ LineNode node = this;
+ while (node.right != null)
+ node = node.right;
+ return node;
+ }
+ }
+
+ /// <summary>
+ /// The number of lines in this node and its child nodes.
+ /// Invariant:
+ /// nodeTotalCount = 1 + left.nodeTotalCount + right.nodeTotalCount
+ /// </summary>
+ internal int nodeTotalCount;
+
+ /// <summary>
+ /// The total text length of this node and its child nodes.
+ /// Invariant:
+ /// nodeTotalLength = left.nodeTotalLength + documentLine.TotalLength + right.nodeTotalLength
+ /// </summary>
+ internal int nodeTotalLength;
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/NewLineFinder.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/NewLineFinder.cs
new file mode 100644
index 000000000..6ecf8c1e5
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/NewLineFinder.cs
@@ -0,0 +1,134 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+namespace Tango.Scripting.Editors.Document
+{
+ static class NewLineFinder
+ {
+ static readonly char[] newline = { '\r', '\n' };
+
+ internal static readonly string[] NewlineStrings = { "\r\n", "\r", "\n" };
+
+ /// <summary>
+ /// Gets the location of the next new line character, or SimpleSegment.Invalid
+ /// if none is found.
+ /// </summary>
+ internal static SimpleSegment NextNewLine(string text, int offset)
+ {
+ int pos = text.IndexOfAny(newline, offset);
+ if (pos >= 0) {
+ if (text[pos] == '\r') {
+ if (pos + 1 < text.Length && text[pos + 1] == '\n')
+ return new SimpleSegment(pos, 2);
+ }
+ return new SimpleSegment(pos, 1);
+ }
+ return SimpleSegment.Invalid;
+ }
+
+ /// <summary>
+ /// Gets the location of the next new line character, or SimpleSegment.Invalid
+ /// if none is found.
+ /// </summary>
+ internal static SimpleSegment NextNewLine(ITextSource text, int offset)
+ {
+ int textLength = text.TextLength;
+ int pos = text.IndexOfAny(newline, offset, textLength - offset);
+ if (pos >= 0) {
+ if (text.GetCharAt(pos) == '\r') {
+ if (pos + 1 < textLength && text.GetCharAt(pos + 1) == '\n')
+ return new SimpleSegment(pos, 2);
+ }
+ return new SimpleSegment(pos, 1);
+ }
+ return SimpleSegment.Invalid;
+ }
+ }
+
+ partial class TextUtilities
+ {
+ /// <summary>
+ /// Finds the next new line character starting at offset.
+ /// </summary>
+ /// <param name="text">The text source to search in.</param>
+ /// <param name="offset">The starting offset for the search.</param>
+ /// <param name="newLineType">The string representing the new line that was found, or null if no new line was found.</param>
+ /// <returns>The position of the first new line starting at or after <paramref name="offset"/>,
+ /// or -1 if no new line was found.</returns>
+ public static int FindNextNewLine(ITextSource text, int offset, out string newLineType)
+ {
+ if (text == null)
+ throw new ArgumentNullException("text");
+ if (offset < 0 || offset > text.TextLength)
+ throw new ArgumentOutOfRangeException("offset", offset, "offset is outside of text source");
+ SimpleSegment s = NewLineFinder.NextNewLine(text, offset);
+ if (s == SimpleSegment.Invalid) {
+ newLineType = null;
+ return -1;
+ } else {
+ if (s.Length == 2) {
+ newLineType = "\r\n";
+ } else if (text.GetCharAt(s.Offset) == '\n') {
+ newLineType = "\n";
+ } else {
+ newLineType = "\r";
+ }
+ return s.Offset;
+ }
+ }
+
+ /// <summary>
+ /// Gets whether the specified string is a newline sequence.
+ /// </summary>
+ public static bool IsNewLine(string newLine)
+ {
+ return newLine == "\r\n" || newLine == "\n" || newLine == "\r";
+ }
+
+ /// <summary>
+ /// Normalizes all new lines in <paramref name="input"/> to be <paramref name="newLine"/>.
+ /// </summary>
+ public static string NormalizeNewLines(string input, string newLine)
+ {
+ if (input == null)
+ return null;
+ if (!IsNewLine(newLine))
+ throw new ArgumentException("newLine must be one of the known newline sequences");
+ SimpleSegment ds = NewLineFinder.NextNewLine(input, 0);
+ if (ds == SimpleSegment.Invalid) // text does not contain any new lines
+ return input;
+ StringBuilder b = new StringBuilder(input.Length);
+ int lastEndOffset = 0;
+ do {
+ b.Append(input, lastEndOffset, ds.Offset - lastEndOffset);
+ b.Append(newLine);
+ lastEndOffset = ds.EndOffset;
+ ds = NewLineFinder.NextNewLine(input, lastEndOffset);
+ } while (ds != SimpleSegment.Invalid);
+ // remaining string (after last newline)
+ b.Append(input, lastEndOffset, input.Length - lastEndOffset);
+ return b.ToString();
+ }
+
+ /// <summary>
+ /// Gets the newline sequence used in the document at the specified line.
+ /// </summary>
+ public static string GetNewLineFromDocument(TextDocument document, int lineNumber)
+ {
+ DocumentLine line = document.GetLineByNumber(lineNumber);
+ if (line.DelimiterLength == 0) {
+ // at the end of the document, there's no line delimiter, so use the delimiter
+ // from the previous line
+ line = line.PreviousLine;
+ if (line == null)
+ return Environment.NewLine;
+ }
+ return document.GetText(line.Offset + line.Length, line.DelimiterLength);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/OffsetChangeMap.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/OffsetChangeMap.cs
new file mode 100644
index 000000000..9494af56b
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/OffsetChangeMap.cs
@@ -0,0 +1,347 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Contains predefined offset change mapping types.
+ /// </summary>
+ public enum OffsetChangeMappingType
+ {
+ /// <summary>
+ /// Normal replace.
+ /// Anchors in front of the replaced region will stay in front, anchors after the replaced region will stay after.
+ /// Anchors in the middle of the removed region will be deleted. If they survive deletion,
+ /// they move depending on their AnchorMovementType.
+ /// </summary>
+ /// <remarks>
+ /// This is the default implementation of DocumentChangeEventArgs when OffsetChangeMap is null,
+ /// so using this option usually works without creating an OffsetChangeMap instance.
+ /// This is equivalent to an OffsetChangeMap with a single entry describing the replace operation.
+ /// </remarks>
+ Normal,
+ /// <summary>
+ /// First the old text is removed, then the new text is inserted.
+ /// Anchors immediately in front (or after) the replaced region may move to the other side of the insertion,
+ /// depending on the AnchorMovementType.
+ /// </summary>
+ /// <remarks>
+ /// This is implemented as an OffsetChangeMap with two entries: the removal, and the insertion.
+ /// </remarks>
+ RemoveAndInsert,
+ /// <summary>
+ /// The text is replaced character-by-character.
+ /// Anchors keep their position inside the replaced text.
+ /// Anchors after the replaced region will move accordingly if the replacement text has a different length than the replaced text.
+ /// If the new text is shorter than the old text, anchors inside the old text that would end up behind the replacement text
+ /// will be moved so that they point to the end of the replacement text.
+ /// </summary>
+ /// <remarks>
+ /// On the OffsetChangeMap level, growing text is implemented by replacing the last character in the replaced text
+ /// with itself and the additional text segment. A simple insertion of the additional text would have the undesired
+ /// effect of moving anchors immediately after the replaced text into the replacement text if they used
+ /// AnchorMovementStyle.BeforeInsertion.
+ /// Shrinking text is implemented by removing the text segment that's too long; but in a special mode that
+ /// causes anchors to always survive irrespective of their <see cref="TextAnchor.SurviveDeletion"/> setting.
+ /// If the text keeps its old size, this is implemented as OffsetChangeMap.Empty.
+ /// </remarks>
+ CharacterReplace,
+ /// <summary>
+ /// Like 'Normal', but anchors with <see cref="TextAnchor.MovementType"/> = Default will stay in front of the
+ /// insertion instead of being moved behind it.
+ /// </summary>
+ KeepAnchorBeforeInsertion
+ }
+
+ /// <summary>
+ /// Describes a series of offset changes.
+ /// </summary>
+ [Serializable]
+ [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix",
+ Justification="It's a mapping old offsets -> new offsets")]
+ public sealed class OffsetChangeMap : Collection<OffsetChangeMapEntry>
+ {
+ /// <summary>
+ /// Immutable OffsetChangeMap that is empty.
+ /// </summary>
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes",
+ Justification="The Empty instance is immutable")]
+ public static readonly OffsetChangeMap Empty = new OffsetChangeMap(Empty<OffsetChangeMapEntry>.Array, true);
+
+ /// <summary>
+ /// Creates a new OffsetChangeMap with a single element.
+ /// </summary>
+ /// <param name="entry">The entry.</param>
+ /// <returns>Returns a frozen OffsetChangeMap with a single entry.</returns>
+ public static OffsetChangeMap FromSingleElement(OffsetChangeMapEntry entry)
+ {
+ return new OffsetChangeMap(new OffsetChangeMapEntry[] { entry }, true);
+ }
+
+ bool isFrozen;
+
+ /// <summary>
+ /// Creates a new OffsetChangeMap instance.
+ /// </summary>
+ public OffsetChangeMap()
+ {
+ }
+
+ internal OffsetChangeMap(int capacity)
+ : base(new List<OffsetChangeMapEntry>(capacity))
+ {
+ }
+
+ private OffsetChangeMap(IList<OffsetChangeMapEntry> entries, bool isFrozen)
+ : base(entries)
+ {
+ this.isFrozen = isFrozen;
+ }
+
+ /// <summary>
+ /// Gets the new offset where the specified offset moves after this document change.
+ /// </summary>
+ public int GetNewOffset(int offset, AnchorMovementType movementType)
+ {
+ IList<OffsetChangeMapEntry> items = this.Items;
+ int count = items.Count;
+ for (int i = 0; i < count; i++) {
+ offset = items[i].GetNewOffset(offset, movementType);
+ }
+ return offset;
+ }
+
+ /// <summary>
+ /// Gets whether this OffsetChangeMap is a valid explanation for the specified document change.
+ /// </summary>
+ public bool IsValidForDocumentChange(int offset, int removalLength, int insertionLength)
+ {
+ int endOffset = offset + removalLength;
+ foreach (OffsetChangeMapEntry entry in this) {
+ // check that ChangeMapEntry is in valid range for this document change
+ if (entry.Offset < offset || entry.Offset + entry.RemovalLength > endOffset)
+ return false;
+ endOffset += entry.InsertionLength - entry.RemovalLength;
+ }
+ // check that the total delta matches
+ return endOffset == offset + insertionLength;
+ }
+
+ /// <summary>
+ /// Calculates the inverted OffsetChangeMap (used for the undo operation).
+ /// </summary>
+ public OffsetChangeMap Invert()
+ {
+ if (this == Empty)
+ return this;
+ OffsetChangeMap newMap = new OffsetChangeMap(this.Count);
+ for (int i = this.Count - 1; i >= 0; i--) {
+ OffsetChangeMapEntry entry = this[i];
+ // swap InsertionLength and RemovalLength
+ newMap.Add(new OffsetChangeMapEntry(entry.Offset, entry.InsertionLength, entry.RemovalLength));
+ }
+ return newMap;
+ }
+
+ /// <inheritdoc/>
+ protected override void ClearItems()
+ {
+ CheckFrozen();
+ base.ClearItems();
+ }
+
+ /// <inheritdoc/>
+ protected override void InsertItem(int index, OffsetChangeMapEntry item)
+ {
+ CheckFrozen();
+ base.InsertItem(index, item);
+ }
+
+ /// <inheritdoc/>
+ protected override void RemoveItem(int index)
+ {
+ CheckFrozen();
+ base.RemoveItem(index);
+ }
+
+ /// <inheritdoc/>
+ protected override void SetItem(int index, OffsetChangeMapEntry item)
+ {
+ CheckFrozen();
+ base.SetItem(index, item);
+ }
+
+ void CheckFrozen()
+ {
+ if (isFrozen)
+ throw new InvalidOperationException("This instance is frozen and cannot be modified.");
+ }
+
+ /// <summary>
+ /// Gets if this instance is frozen. Frozen instances are immutable and thus thread-safe.
+ /// </summary>
+ public bool IsFrozen {
+ get { return isFrozen; }
+ }
+
+ /// <summary>
+ /// Freezes this instance.
+ /// </summary>
+ public void Freeze()
+ {
+ isFrozen = true;
+ }
+ }
+
+ /// <summary>
+ /// An entry in the OffsetChangeMap.
+ /// This represents the offset of a document change (either insertion or removal, not both at once).
+ /// </summary>
+ [Serializable]
+ public struct OffsetChangeMapEntry : IEquatable<OffsetChangeMapEntry>
+ {
+ readonly int offset;
+
+ // MSB: DefaultAnchorMovementIsBeforeInsertion
+ readonly uint insertionLengthWithMovementFlag;
+
+ // MSB: RemovalNeverCausesAnchorDeletion; other 31 bits: RemovalLength
+ readonly uint removalLengthWithDeletionFlag;
+
+ /// <summary>
+ /// The offset at which the change occurs.
+ /// </summary>
+ public int Offset {
+ get { return offset; }
+ }
+
+ /// <summary>
+ /// The number of characters inserted.
+ /// Returns 0 if this entry represents a removal.
+ /// </summary>
+ public int InsertionLength {
+ get { return (int)(insertionLengthWithMovementFlag & 0x7fffffff); }
+ }
+
+ /// <summary>
+ /// The number of characters removed.
+ /// Returns 0 if this entry represents an insertion.
+ /// </summary>
+ public int RemovalLength {
+ get { return (int)(removalLengthWithDeletionFlag & 0x7fffffff); }
+ }
+
+ /// <summary>
+ /// Gets whether the removal should not cause any anchor deletions.
+ /// </summary>
+ public bool RemovalNeverCausesAnchorDeletion {
+ get { return (removalLengthWithDeletionFlag & 0x80000000) != 0; }
+ }
+
+ /// <summary>
+ /// Gets whether default anchor movement causes the anchor to stay in front of the caret.
+ /// </summary>
+ public bool DefaultAnchorMovementIsBeforeInsertion {
+ get { return (insertionLengthWithMovementFlag & 0x80000000) != 0; }
+ }
+
+ /// <summary>
+ /// Gets the new offset where the specified offset moves after this document change.
+ /// </summary>
+ public int GetNewOffset(int oldOffset, AnchorMovementType movementType)
+ {
+ int insertionLength = this.InsertionLength;
+ int removalLength = this.RemovalLength;
+ if (!(removalLength == 0 && oldOffset == offset)) {
+ // we're getting trouble (both if statements in here would apply)
+ // if there's no removal and we insert at the offset
+ // -> we'd need to disambiguate by movementType, which is handled after the if
+
+ // offset is before start of change: no movement
+ if (oldOffset <= offset)
+ return oldOffset;
+ // offset is after end of change: movement by normal delta
+ if (oldOffset >= offset + removalLength)
+ return oldOffset + insertionLength - removalLength;
+ }
+ // we reach this point if
+ // a) the oldOffset is inside the deleted segment
+ // b) there was no removal and we insert at the caret position
+ if (movementType == AnchorMovementType.AfterInsertion)
+ return offset + insertionLength;
+ else if (movementType == AnchorMovementType.BeforeInsertion)
+ return offset;
+ else
+ return this.DefaultAnchorMovementIsBeforeInsertion ? offset : offset + insertionLength;
+ }
+
+ /// <summary>
+ /// Creates a new OffsetChangeMapEntry instance.
+ /// </summary>
+ public OffsetChangeMapEntry(int offset, int removalLength, int insertionLength)
+ {
+ ThrowUtil.CheckNotNegative(offset, "offset");
+ ThrowUtil.CheckNotNegative(removalLength, "removalLength");
+ ThrowUtil.CheckNotNegative(insertionLength, "insertionLength");
+
+ this.offset = offset;
+ this.removalLengthWithDeletionFlag = (uint)removalLength;
+ this.insertionLengthWithMovementFlag = (uint)insertionLength;
+ }
+
+ /// <summary>
+ /// Creates a new OffsetChangeMapEntry instance.
+ /// </summary>
+ public OffsetChangeMapEntry(int offset, int removalLength, int insertionLength, bool removalNeverCausesAnchorDeletion, bool defaultAnchorMovementIsBeforeInsertion)
+ : this(offset, removalLength, insertionLength)
+ {
+ if (removalNeverCausesAnchorDeletion)
+ this.removalLengthWithDeletionFlag |= 0x80000000;
+ if (defaultAnchorMovementIsBeforeInsertion)
+ this.insertionLengthWithMovementFlag |= 0x80000000;
+ }
+
+ /// <inheritdoc/>
+ public override int GetHashCode()
+ {
+ unchecked {
+ return offset + 3559 * (int)insertionLengthWithMovementFlag + 3571 * (int)removalLengthWithDeletionFlag;
+ }
+ }
+
+ /// <inheritdoc/>
+ public override bool Equals(object obj)
+ {
+ return obj is OffsetChangeMapEntry && this.Equals((OffsetChangeMapEntry)obj);
+ }
+
+ /// <inheritdoc/>
+ public bool Equals(OffsetChangeMapEntry other)
+ {
+ return offset == other.offset && insertionLengthWithMovementFlag == other.insertionLengthWithMovementFlag && removalLengthWithDeletionFlag == other.removalLengthWithDeletionFlag;
+ }
+
+ /// <summary>
+ /// Tests the two entries for equality.
+ /// </summary>
+ public static bool operator ==(OffsetChangeMapEntry left, OffsetChangeMapEntry right)
+ {
+ return left.Equals(right);
+ }
+
+ /// <summary>
+ /// Tests the two entries for inequality.
+ /// </summary>
+ public static bool operator !=(OffsetChangeMapEntry left, OffsetChangeMapEntry right)
+ {
+ return !left.Equals(right);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchor.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchor.cs
new file mode 100644
index 000000000..a65c276f9
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchor.cs
@@ -0,0 +1,194 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// The TextAnchor class references an offset (a position between two characters).
+ /// It automatically updates the offset when text is inserted/removed in front of the anchor.
+ /// </summary>
+ /// <remarks>
+ /// <para>Use the <see cref="Offset"/> property to get the offset from a text anchor.
+ /// Use the <see cref="TextDocument.CreateAnchor"/> method to create an anchor from an offset.
+ /// </para>
+ /// <para>
+ /// The document will automatically update all text anchors; and because it uses weak references to do so,
+ /// the garbage collector can simply collect the anchor object when you don't need it anymore.
+ /// </para>
+ /// <para>Moreover, the document is able to efficiently update a large number of anchors without having to look
+ /// at each anchor object individually. Updating the offsets of all anchors usually only takes time logarithmic
+ /// to the number of anchors. Retrieving the <see cref="Offset"/> property also runs in O(lg N).</para>
+ /// <inheritdoc cref="IsDeleted" />
+ /// <inheritdoc cref="MovementType" />
+ /// <para>If you want to track a segment, you can use the <see cref="AnchorSegment"/> class which
+ /// implements <see cref="ISegment"/> using two text anchors.</para>
+ /// </remarks>
+ /// <example>
+ /// Usage:
+ /// <code>TextAnchor anchor = document.CreateAnchor(offset);
+ /// ChangeMyDocument();
+ /// int newOffset = anchor.Offset;
+ /// </code>
+ /// </example>
+ public sealed class TextAnchor
+ {
+ readonly TextDocument document;
+ internal TextAnchorNode node;
+
+ internal TextAnchor(TextDocument document)
+ {
+ this.document = document;
+ }
+
+ /// <summary>
+ /// Gets the document owning the anchor.
+ /// </summary>
+ public TextDocument Document {
+ get { return document; }
+ }
+
+ /// <summary>
+ /// Controls how the anchor moves.
+ /// </summary>
+ /// <remarks>Anchor movement is ambiguous if text is inserted exactly at the anchor's location.
+ /// Does the anchor stay before the inserted text, or does it move after it?
+ /// The property <see cref="MovementType"/> will be used to determine which of these two options the anchor will choose.
+ /// The default value is <see cref="AnchorMovementType.Default"/>.</remarks>
+ public AnchorMovementType MovementType { get; set; }
+
+ /// <summary>
+ /// <para>
+ /// Specifies whether the anchor survives deletion of the text containing it.
+ /// </para><para>
+ /// <c>false</c>: The anchor is deleted when the a selection that includes the anchor is deleted.
+ /// <c>true</c>: The anchor is not deleted.
+ /// </para>
+ /// </summary>
+ /// <remarks><inheritdoc cref="IsDeleted" /></remarks>
+ public bool SurviveDeletion { get; set; }
+
+ /// <summary>
+ /// Gets whether the anchor was deleted.
+ /// </summary>
+ /// <remarks>
+ /// <para>When a piece of text containing an anchor is removed, then that anchor will be deleted.
+ /// First, the <see cref="IsDeleted"/> property is set to true on all deleted anchors,
+ /// then the <see cref="Deleted"/> events are raised.
+ /// You cannot retrieve the offset from an anchor that has been deleted.</para>
+ /// <para>This deletion behavior might be useful when using anchors for building a bookmark feature,
+ /// but in other cases you want to still be able to use the anchor. For those cases, set <c><see cref="SurviveDeletion"/> = true</c>.</para>
+ /// </remarks>
+ public bool IsDeleted {
+ get {
+ document.DebugVerifyAccess();
+ return node == null;
+ }
+ }
+
+ /// <summary>
+ /// Occurs after the anchor was deleted.
+ /// </summary>
+ /// <remarks>
+ /// <inheritdoc cref="IsDeleted" />
+ /// <para>Due to the 'weak reference' nature of TextAnchor, you will receive the Deleted event only
+ /// while your code holds a reference to the TextAnchor object.</para>
+ /// </remarks>
+ public event EventHandler Deleted;
+
+ internal void OnDeleted(DelayedEvents delayedEvents)
+ {
+ node = null;
+ delayedEvents.DelayedRaise(Deleted, this, EventArgs.Empty);
+ }
+
+ /// <summary>
+ /// Gets the offset of the text anchor.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
+ public int Offset {
+ get {
+ document.DebugVerifyAccess();
+
+ TextAnchorNode n = this.node;
+ if (n == null)
+ throw new InvalidOperationException();
+
+ int offset = n.length;
+ if (n.left != null)
+ offset += n.left.totalLength;
+ while (n.parent != null) {
+ if (n == n.parent.right) {
+ if (n.parent.left != null)
+ offset += n.parent.left.totalLength;
+ offset += n.parent.length;
+ }
+ n = n.parent;
+ }
+ return offset;
+ }
+ }
+
+ /// <summary>
+ /// Gets the line number of the anchor.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
+ public int Line {
+ get {
+ return document.GetLineByOffset(this.Offset).LineNumber;
+ }
+ }
+
+ /// <summary>
+ /// Gets the column number of this anchor.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
+ public int Column {
+ get {
+ int offset = this.Offset;
+ return offset - document.GetLineByOffset(offset).Offset + 1;
+ }
+ }
+
+ /// <summary>
+ /// Gets the text location of this anchor.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
+ public TextLocation Location {
+ get {
+ return document.GetLocation(this.Offset);
+ }
+ }
+
+ /// <inheritdoc/>
+ public override string ToString()
+ {
+ return "[TextAnchor Offset=" + Offset + "]";
+ }
+ }
+
+ /// <summary>
+ /// Defines how a text anchor moves.
+ /// </summary>
+ public enum AnchorMovementType
+ {
+ /// <summary>
+ /// When text is inserted at the anchor position, the type of the insertion
+ /// determines where the caret moves to. For normal insertions, the anchor will stay
+ /// behind the inserted text.
+ /// </summary>
+ Default,
+ /// <summary>
+ /// When text is inserted at the anchor position, the anchor will stay
+ /// before the inserted text.
+ /// </summary>
+ BeforeInsertion,
+ /// <summary>
+ /// When text is insered at the anchor position, the anchor will move
+ /// after the inserted text.
+ /// </summary>
+ AfterInsertion
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchorNode.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchorNode.cs
new file mode 100644
index 000000000..bfa7c4eef
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchorNode.cs
@@ -0,0 +1,87 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// A TextAnchorNode is placed in the TextAnchorTree.
+ /// It describes a section of text with a text anchor at the end of the section.
+ /// A weak reference is used to refer to the TextAnchor. (to save memory, we derive from WeakReference instead of referencing it)
+ /// </summary>
+ sealed class TextAnchorNode : WeakReference
+ {
+ internal TextAnchorNode left, right, parent;
+ internal bool color;
+ internal int length;
+ internal int totalLength; // totalLength = length + left.totalLength + right.totalLength
+
+ public TextAnchorNode(TextAnchor anchor) : base(anchor)
+ {
+ }
+
+ internal TextAnchorNode LeftMost {
+ get {
+ TextAnchorNode node = this;
+ while (node.left != null)
+ node = node.left;
+ return node;
+ }
+ }
+
+ internal TextAnchorNode RightMost {
+ get {
+ TextAnchorNode node = this;
+ while (node.right != null)
+ node = node.right;
+ return node;
+ }
+ }
+
+ /// <summary>
+ /// Gets the inorder successor of the node.
+ /// </summary>
+ internal TextAnchorNode Successor {
+ get {
+ if (right != null) {
+ return right.LeftMost;
+ } else {
+ TextAnchorNode node = this;
+ TextAnchorNode oldNode;
+ do {
+ oldNode = node;
+ node = node.parent;
+ // go up until we are coming out of a left subtree
+ } while (node != null && node.right == oldNode);
+ return node;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the inorder predecessor of the node.
+ /// </summary>
+ internal TextAnchorNode Predecessor {
+ get {
+ if (left != null) {
+ return left.RightMost;
+ } else {
+ TextAnchorNode node = this;
+ TextAnchorNode oldNode;
+ do {
+ oldNode = node;
+ node = node.parent;
+ // go up until we are coming out of a right subtree
+ } while (node != null && node.left == oldNode);
+ return node;
+ }
+ }
+ }
+
+ public override string ToString()
+ {
+ return "[TextAnchorNode Length=" + length + " TotalLength=" + totalLength + " Target=" + Target + "]";
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchorTree.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchorTree.cs
new file mode 100644
index 000000000..d07a33b9d
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextAnchorTree.cs
@@ -0,0 +1,753 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// A tree of TextAnchorNodes.
+ /// </summary>
+ sealed class TextAnchorTree
+ {
+ // The text anchor tree has difficult requirements:
+ // - it must QUICKLY update the offset in all anchors whenever there is a document change
+ // - it must not reference text anchors directly, using weak references instead
+
+ // Clearly, we cannot afford updating an Offset property on all anchors (that would be O(N)).
+ // So instead, the anchors need to be able to calculate their offset from a data structure
+ // that can be efficiently updated.
+
+ // This implementation is built using an augmented red-black-tree.
+ // There is a 'TextAnchorNode' for each text anchor.
+ // Such a node represents a section of text (just the length is stored) with a (weakly referenced) text anchor at the end.
+
+ // Basically, you can imagine the list of text anchors as a sorted list of text anchors, where each anchor
+ // just stores the distance to the previous anchor.
+ // (next node = TextAnchorNode.Successor, distance = TextAnchorNode.length)
+ // Distances are never negative, so this representation means anchors are always sorted by offset
+ // (the order of anchors at the same offset is undefined)
+
+ // Of course, a linked list of anchors would be way too slow (one would need to traverse the whole list
+ // every time the offset of an anchor is being looked up).
+ // Instead, we use a red-black-tree. We aren't actually using the tree for sorting - it's just a binary tree
+ // as storage format for what's conceptually a list, the red-black properties are used to keep the tree balanced.
+ // Other balanced binary trees would work, too.
+
+ // What makes the tree-form efficient is that is augments the data by a 'totalLength'. Where 'length'
+ // represents the distance to the previous node, 'totalLength' is the sum of all 'length' values in the subtree
+ // under that node.
+ // This allows computing the Offset from an anchor by walking up the list of parent nodes instead of going
+ // through all predecessor nodes. So computing the Offset runs in O(log N).
+
+ readonly TextDocument document;
+ readonly List<TextAnchorNode> nodesToDelete = new List<TextAnchorNode>();
+ TextAnchorNode root;
+
+ public TextAnchorTree(TextDocument document)
+ {
+ this.document = document;
+ }
+
+ [Conditional("DEBUG")]
+ static void Log(string text)
+ {
+ Debug.WriteLine("TextAnchorTree: " + text);
+ }
+
+ #region Insert Text
+ void InsertText(int offset, int length, bool defaultAnchorMovementIsBeforeInsertion)
+ {
+ if (length == 0 || root == null || offset > root.totalLength)
+ return;
+
+ // find the range of nodes that are placed exactly at offset
+ // beginNode is inclusive, endNode is exclusive
+ if (offset == root.totalLength) {
+ PerformInsertText(FindActualBeginNode(root.RightMost), null, length, defaultAnchorMovementIsBeforeInsertion);
+ } else {
+ TextAnchorNode endNode = FindNode(ref offset);
+ Debug.Assert(endNode.length > 0);
+
+ if (offset > 0) {
+ // there are no nodes exactly at offset
+ endNode.length += length;
+ UpdateAugmentedData(endNode);
+ } else {
+ PerformInsertText(FindActualBeginNode(endNode.Predecessor), endNode, length, defaultAnchorMovementIsBeforeInsertion);
+ }
+ }
+ DeleteMarkedNodes();
+ }
+
+ TextAnchorNode FindActualBeginNode(TextAnchorNode node)
+ {
+ // now find the actual beginNode
+ while (node != null && node.length == 0)
+ node = node.Predecessor;
+ if (node == null) {
+ // no predecessor = beginNode is first node in tree
+ node = root.LeftMost;
+ }
+ return node;
+ }
+
+ // Sorts the nodes in the range [beginNode, endNode) by MovementType
+ // and inserts the length between the BeforeInsertion and the AfterInsertion nodes.
+ void PerformInsertText(TextAnchorNode beginNode, TextAnchorNode endNode, int length, bool defaultAnchorMovementIsBeforeInsertion)
+ {
+ Debug.Assert(beginNode != null);
+ // endNode may be null at the end of the anchor tree
+
+ // now we need to sort the nodes in the range [beginNode, endNode); putting those with
+ // MovementType.BeforeInsertion in front of those with MovementType.AfterInsertion
+ List<TextAnchorNode> beforeInsert = new List<TextAnchorNode>();
+ //List<TextAnchorNode> afterInsert = new List<TextAnchorNode>();
+ TextAnchorNode temp = beginNode;
+ while (temp != endNode) {
+ TextAnchor anchor = (TextAnchor)temp.Target;
+ if (anchor == null) {
+ // afterInsert.Add(temp);
+ MarkNodeForDelete(temp);
+ } else if (defaultAnchorMovementIsBeforeInsertion
+ ? anchor.MovementType != AnchorMovementType.AfterInsertion
+ : anchor.MovementType == AnchorMovementType.BeforeInsertion)
+ {
+ beforeInsert.Add(temp);
+// } else {
+// afterInsert.Add(temp);
+ }
+ temp = temp.Successor;
+ }
+ // now again go through the range and swap the nodes with those in the beforeInsert list
+ temp = beginNode;
+ foreach (TextAnchorNode node in beforeInsert) {
+ SwapAnchors(node, temp);
+ temp = temp.Successor;
+ }
+ // now temp is pointing to the first node that is afterInsert,
+ // or to endNode, if there is no afterInsert node at the offset
+ // So add the length to temp
+ if (temp == null) {
+ // temp might be null if endNode==null and no afterInserts
+ Debug.Assert(endNode == null);
+ } else {
+ temp.length += length;
+ UpdateAugmentedData(temp);
+ }
+ }
+
+ /// <summary>
+ /// Swaps the anchors stored in the two nodes.
+ /// </summary>
+ void SwapAnchors(TextAnchorNode n1, TextAnchorNode n2)
+ {
+ if (n1 != n2) {
+ TextAnchor anchor1 = (TextAnchor)n1.Target;
+ TextAnchor anchor2 = (TextAnchor)n2.Target;
+ if (anchor1 == null && anchor2 == null) {
+ // -> no swap required
+ return;
+ }
+ n1.Target = anchor2;
+ n2.Target = anchor1;
+ if (anchor1 == null) {
+ // unmark n1 from deletion, mark n2 for deletion
+ nodesToDelete.Remove(n1);
+ MarkNodeForDelete(n2);
+ anchor2.node = n1;
+ } else if (anchor2 == null) {
+ // unmark n2 from deletion, mark n1 for deletion
+ nodesToDelete.Remove(n2);
+ MarkNodeForDelete(n1);
+ anchor1.node = n2;
+ } else {
+ anchor1.node = n2;
+ anchor2.node = n1;
+ }
+ }
+ }
+ #endregion
+
+ #region Remove or Replace text
+ public void HandleTextChange(OffsetChangeMapEntry entry, DelayedEvents delayedEvents)
+ {
+ //Log("HandleTextChange(" + entry + ")");
+ if (entry.RemovalLength == 0) {
+ // This is a pure insertion.
+ // Unlike a replace with removal, a pure insertion can result in nodes at the same location
+ // to split depending on their MovementType.
+ // Thus, we handle this case on a separate code path
+ // (the code below looks like it does something similar, but it can only split
+ // the set of deletion survivors, not all nodes at an offset)
+ InsertText(entry.Offset, entry.InsertionLength, entry.DefaultAnchorMovementIsBeforeInsertion);
+ return;
+ }
+ // When handling a replacing text change, we need to:
+ // - find all anchors in the deleted segment and delete them / move them to the appropriate
+ // surviving side.
+ // - adjust the segment size between the left and right side
+
+ int offset = entry.Offset;
+ int remainingRemovalLength = entry.RemovalLength;
+ // if the text change is happening after the last anchor, we don't have to do anything
+ if (root == null || offset >= root.totalLength)
+ return;
+ TextAnchorNode node = FindNode(ref offset);
+ TextAnchorNode firstDeletionSurvivor = null;
+ // go forward through the tree and delete all nodes in the removal segment
+ while (node != null && offset + remainingRemovalLength > node.length) {
+ TextAnchor anchor = (TextAnchor)node.Target;
+ if (anchor != null && (anchor.SurviveDeletion || entry.RemovalNeverCausesAnchorDeletion)) {
+ if (firstDeletionSurvivor == null)
+ firstDeletionSurvivor = node;
+ // This node should be deleted, but it wants to survive.
+ // We'll just remove the deleted length segment, so the node will be positioned
+ // in front of the removed segment.
+ remainingRemovalLength -= node.length - offset;
+ node.length = offset;
+ offset = 0;
+ UpdateAugmentedData(node);
+ node = node.Successor;
+ } else {
+ // delete node
+ TextAnchorNode s = node.Successor;
+ remainingRemovalLength -= node.length;
+ RemoveNode(node);
+ // we already deleted the node, don't delete it twice
+ nodesToDelete.Remove(node);
+ if (anchor != null)
+ anchor.OnDeleted(delayedEvents);
+ node = s;
+ }
+ }
+ // 'node' now is the first anchor after the deleted segment.
+ // If there are no anchors after the deleted segment, 'node' is null.
+
+ // firstDeletionSurvivor was set to the first node surviving deletion.
+ // Because all non-surviving nodes up to 'node' were deleted, the node range
+ // [firstDeletionSurvivor, node) now refers to the set of all deletion survivors.
+
+ // do the remaining job of the removal
+ if (node != null) {
+ node.length -= remainingRemovalLength;
+ Debug.Assert(node.length >= 0);
+ }
+ if (entry.InsertionLength > 0) {
+ // we are performing a replacement
+ if (firstDeletionSurvivor != null) {
+ // We got deletion survivors which need to be split into BeforeInsertion
+ // and AfterInsertion groups.
+ // Take care that we don't regroup everything at offset, but only the deletion
+ // survivors - from firstDeletionSurvivor (inclusive) to node (exclusive).
+ // This ensures that nodes immediately before or after the replaced segment
+ // stay where they are (independent from their MovementType)
+ PerformInsertText(firstDeletionSurvivor, node, entry.InsertionLength, entry.DefaultAnchorMovementIsBeforeInsertion);
+ } else if (node != null) {
+ // No deletion survivors:
+ // just perform the insertion
+ node.length += entry.InsertionLength;
+ }
+ }
+ if (node != null) {
+ UpdateAugmentedData(node);
+ }
+ DeleteMarkedNodes();
+ }
+ #endregion
+
+ #region Node removal when TextAnchor was GC'ed
+ void MarkNodeForDelete(TextAnchorNode node)
+ {
+ if (!nodesToDelete.Contains(node))
+ nodesToDelete.Add(node);
+ }
+
+ void DeleteMarkedNodes()
+ {
+ CheckProperties();
+ while (nodesToDelete.Count > 0) {
+ int pos = nodesToDelete.Count - 1;
+ TextAnchorNode n = nodesToDelete[pos];
+ // combine section of n with the following section
+ TextAnchorNode s = n.Successor;
+ if (s != null) {
+ s.length += n.length;
+ }
+ RemoveNode(n);
+ if (s != null) {
+ UpdateAugmentedData(s);
+ }
+ nodesToDelete.RemoveAt(pos);
+ CheckProperties();
+ }
+ CheckProperties();
+ }
+ #endregion
+
+ #region FindNode
+ /// <summary>
+ /// Finds the node at the specified offset.
+ /// After the method has run, offset is relative to the beginning of the returned node.
+ /// </summary>
+ TextAnchorNode FindNode(ref int offset)
+ {
+ TextAnchorNode n = root;
+ while (true) {
+ if (n.left != null) {
+ if (offset < n.left.totalLength) {
+ n = n.left; // descend into left subtree
+ continue;
+ } else {
+ offset -= n.left.totalLength; // skip left subtree
+ }
+ }
+ if (!n.IsAlive)
+ MarkNodeForDelete(n);
+ if (offset < n.length) {
+ return n; // found correct node
+ } else {
+ offset -= n.length; // skip this node
+ }
+ if (n.right != null) {
+ n = n.right; // descend into right subtree
+ } else {
+ // didn't find any node containing the offset
+ return null;
+ }
+ }
+ }
+ #endregion
+
+ #region UpdateAugmentedData
+ void UpdateAugmentedData(TextAnchorNode n)
+ {
+ if (!n.IsAlive)
+ MarkNodeForDelete(n);
+
+ int totalLength = n.length;
+ if (n.left != null)
+ totalLength += n.left.totalLength;
+ if (n.right != null)
+ totalLength += n.right.totalLength;
+ if (n.totalLength != totalLength) {
+ n.totalLength = totalLength;
+ if (n.parent != null)
+ UpdateAugmentedData(n.parent);
+ }
+ }
+ #endregion
+
+ #region CreateAnchor
+ public TextAnchor CreateAnchor(int offset)
+ {
+ Log("CreateAnchor(" + offset + ")");
+ TextAnchor anchor = new TextAnchor(document);
+ anchor.node = new TextAnchorNode(anchor);
+ if (root == null) {
+ // creating the first text anchor
+ root = anchor.node;
+ root.totalLength = root.length = offset;
+ } else if (offset >= root.totalLength) {
+ // append anchor at end of tree
+ anchor.node.totalLength = anchor.node.length = offset - root.totalLength;
+ InsertAsRight(root.RightMost, anchor.node);
+ } else {
+ // insert anchor in middle of tree
+ TextAnchorNode n = FindNode(ref offset);
+ Debug.Assert(offset < n.length);
+ // split segment 'n' at offset
+ anchor.node.totalLength = anchor.node.length = offset;
+ n.length -= offset;
+ InsertBefore(n, anchor.node);
+ }
+ DeleteMarkedNodes();
+ return anchor;
+ }
+
+ void InsertBefore(TextAnchorNode node, TextAnchorNode newNode)
+ {
+ if (node.left == null) {
+ InsertAsLeft(node, newNode);
+ } else {
+ InsertAsRight(node.left.RightMost, newNode);
+ }
+ }
+ #endregion
+
+ #region Red/Black Tree
+ internal const bool RED = true;
+ internal const bool BLACK = false;
+
+ void InsertAsLeft(TextAnchorNode parentNode, TextAnchorNode newNode)
+ {
+ Debug.Assert(parentNode.left == null);
+ parentNode.left = newNode;
+ newNode.parent = parentNode;
+ newNode.color = RED;
+ UpdateAugmentedData(parentNode);
+ FixTreeOnInsert(newNode);
+ }
+
+ void InsertAsRight(TextAnchorNode parentNode, TextAnchorNode newNode)
+ {
+ Debug.Assert(parentNode.right == null);
+ parentNode.right = newNode;
+ newNode.parent = parentNode;
+ newNode.color = RED;
+ UpdateAugmentedData(parentNode);
+ FixTreeOnInsert(newNode);
+ }
+
+ void FixTreeOnInsert(TextAnchorNode node)
+ {
+ Debug.Assert(node != null);
+ Debug.Assert(node.color == RED);
+ Debug.Assert(node.left == null || node.left.color == BLACK);
+ Debug.Assert(node.right == null || node.right.color == BLACK);
+
+ TextAnchorNode parentNode = node.parent;
+ if (parentNode == null) {
+ // we inserted in the root -> the node must be black
+ // since this is a root node, making the node black increments the number of black nodes
+ // on all paths by one, so it is still the same for all paths.
+ node.color = BLACK;
+ return;
+ }
+ if (parentNode.color == BLACK) {
+ // if the parent node where we inserted was black, our red node is placed correctly.
+ // since we inserted a red node, the number of black nodes on each path is unchanged
+ // -> the tree is still balanced
+ return;
+ }
+ // parentNode is red, so there is a conflict here!
+
+ // because the root is black, parentNode is not the root -> there is a grandparent node
+ TextAnchorNode grandparentNode = parentNode.parent;
+ TextAnchorNode uncleNode = Sibling(parentNode);
+ if (uncleNode != null && uncleNode.color == RED) {
+ parentNode.color = BLACK;
+ uncleNode.color = BLACK;
+ grandparentNode.color = RED;
+ FixTreeOnInsert(grandparentNode);
+ return;
+ }
+ // now we know: parent is red but uncle is black
+ // First rotation:
+ if (node == parentNode.right && parentNode == grandparentNode.left) {
+ RotateLeft(parentNode);
+ node = node.left;
+ } else if (node == parentNode.left && parentNode == grandparentNode.right) {
+ RotateRight(parentNode);
+ node = node.right;
+ }
+ // because node might have changed, reassign variables:
+ parentNode = node.parent;
+ grandparentNode = parentNode.parent;
+
+ // Now recolor a bit:
+ parentNode.color = BLACK;
+ grandparentNode.color = RED;
+ // Second rotation:
+ if (node == parentNode.left && parentNode == grandparentNode.left) {
+ RotateRight(grandparentNode);
+ } else {
+ // because of the first rotation, this is guaranteed:
+ Debug.Assert(node == parentNode.right && parentNode == grandparentNode.right);
+ RotateLeft(grandparentNode);
+ }
+ }
+
+ void RemoveNode(TextAnchorNode removedNode)
+ {
+ if (removedNode.left != null && removedNode.right != null) {
+ // replace removedNode with it's in-order successor
+
+ TextAnchorNode leftMost = removedNode.right.LeftMost;
+ RemoveNode(leftMost); // remove leftMost from its current location
+
+ // and overwrite the removedNode with it
+ ReplaceNode(removedNode, leftMost);
+ leftMost.left = removedNode.left;
+ if (leftMost.left != null) leftMost.left.parent = leftMost;
+ leftMost.right = removedNode.right;
+ if (leftMost.right != null) leftMost.right.parent = leftMost;
+ leftMost.color = removedNode.color;
+
+ UpdateAugmentedData(leftMost);
+ if (leftMost.parent != null) UpdateAugmentedData(leftMost.parent);
+ return;
+ }
+
+ // now either removedNode.left or removedNode.right is null
+ // get the remaining child
+ TextAnchorNode parentNode = removedNode.parent;
+ TextAnchorNode childNode = removedNode.left ?? removedNode.right;
+ ReplaceNode(removedNode, childNode);
+ if (parentNode != null) UpdateAugmentedData(parentNode);
+ if (removedNode.color == BLACK) {
+ if (childNode != null && childNode.color == RED) {
+ childNode.color = BLACK;
+ } else {
+ FixTreeOnDelete(childNode, parentNode);
+ }
+ }
+ }
+
+ void FixTreeOnDelete(TextAnchorNode node, TextAnchorNode parentNode)
+ {
+ Debug.Assert(node == null || node.parent == parentNode);
+ if (parentNode == null)
+ return;
+
+ // warning: node may be null
+ TextAnchorNode sibling = Sibling(node, parentNode);
+ if (sibling.color == RED) {
+ parentNode.color = RED;
+ sibling.color = BLACK;
+ if (node == parentNode.left) {
+ RotateLeft(parentNode);
+ } else {
+ RotateRight(parentNode);
+ }
+
+ sibling = Sibling(node, parentNode); // update value of sibling after rotation
+ }
+
+ if (parentNode.color == BLACK
+ && sibling.color == BLACK
+ && GetColor(sibling.left) == BLACK
+ && GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ FixTreeOnDelete(parentNode, parentNode.parent);
+ return;
+ }
+
+ if (parentNode.color == RED
+ && sibling.color == BLACK
+ && GetColor(sibling.left) == BLACK
+ && GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ parentNode.color = BLACK;
+ return;
+ }
+
+ if (node == parentNode.left &&
+ sibling.color == BLACK &&
+ GetColor(sibling.left) == RED &&
+ GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ sibling.left.color = BLACK;
+ RotateRight(sibling);
+ }
+ else if (node == parentNode.right &&
+ sibling.color == BLACK &&
+ GetColor(sibling.right) == RED &&
+ GetColor(sibling.left) == BLACK)
+ {
+ sibling.color = RED;
+ sibling.right.color = BLACK;
+ RotateLeft(sibling);
+ }
+ sibling = Sibling(node, parentNode); // update value of sibling after rotation
+
+ sibling.color = parentNode.color;
+ parentNode.color = BLACK;
+ if (node == parentNode.left) {
+ if (sibling.right != null) {
+ Debug.Assert(sibling.right.color == RED);
+ sibling.right.color = BLACK;
+ }
+ RotateLeft(parentNode);
+ } else {
+ if (sibling.left != null) {
+ Debug.Assert(sibling.left.color == RED);
+ sibling.left.color = BLACK;
+ }
+ RotateRight(parentNode);
+ }
+ }
+
+ void ReplaceNode(TextAnchorNode replacedNode, TextAnchorNode newNode)
+ {
+ if (replacedNode.parent == null) {
+ Debug.Assert(replacedNode == root);
+ root = newNode;
+ } else {
+ if (replacedNode.parent.left == replacedNode)
+ replacedNode.parent.left = newNode;
+ else
+ replacedNode.parent.right = newNode;
+ }
+ if (newNode != null) {
+ newNode.parent = replacedNode.parent;
+ }
+ replacedNode.parent = null;
+ }
+
+ void RotateLeft(TextAnchorNode p)
+ {
+ // let q be p's right child
+ TextAnchorNode q = p.right;
+ Debug.Assert(q != null);
+ Debug.Assert(q.parent == p);
+ // set q to be the new root
+ ReplaceNode(p, q);
+
+ // set p's right child to be q's left child
+ p.right = q.left;
+ if (p.right != null) p.right.parent = p;
+ // set q's left child to be p
+ q.left = p;
+ p.parent = q;
+ UpdateAugmentedData(p);
+ UpdateAugmentedData(q);
+ }
+
+ void RotateRight(TextAnchorNode p)
+ {
+ // let q be p's left child
+ TextAnchorNode q = p.left;
+ Debug.Assert(q != null);
+ Debug.Assert(q.parent == p);
+ // set q to be the new root
+ ReplaceNode(p, q);
+
+ // set p's left child to be q's right child
+ p.left = q.right;
+ if (p.left != null) p.left.parent = p;
+ // set q's right child to be p
+ q.right = p;
+ p.parent = q;
+ UpdateAugmentedData(p);
+ UpdateAugmentedData(q);
+ }
+
+ static TextAnchorNode Sibling(TextAnchorNode node)
+ {
+ if (node == node.parent.left)
+ return node.parent.right;
+ else
+ return node.parent.left;
+ }
+
+ static TextAnchorNode Sibling(TextAnchorNode node, TextAnchorNode parentNode)
+ {
+ Debug.Assert(node == null || node.parent == parentNode);
+ if (node == parentNode.left)
+ return parentNode.right;
+ else
+ return parentNode.left;
+ }
+
+ static bool GetColor(TextAnchorNode node)
+ {
+ return node != null ? node.color : BLACK;
+ }
+ #endregion
+
+ #region CheckProperties
+ [Conditional("DATACONSISTENCYTEST")]
+ internal void CheckProperties()
+ {
+ #if DEBUG
+ if (root != null) {
+ CheckProperties(root);
+
+ // check red-black property:
+ int blackCount = -1;
+ CheckNodeProperties(root, null, RED, 0, ref blackCount);
+ }
+ #endif
+ }
+
+ #if DEBUG
+ void CheckProperties(TextAnchorNode node)
+ {
+ int totalLength = node.length;
+ if (node.left != null) {
+ CheckProperties(node.left);
+ totalLength += node.left.totalLength;
+ }
+ if (node.right != null) {
+ CheckProperties(node.right);
+ totalLength += node.right.totalLength;
+ }
+ Debug.Assert(node.totalLength == totalLength);
+ }
+
+ /*
+ 1. A node is either red or black.
+ 2. The root is black.
+ 3. All leaves are black. (The leaves are the NIL children.)
+ 4. Both children of every red node are black. (So every red node must have a black parent.)
+ 5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.)
+ */
+ void CheckNodeProperties(TextAnchorNode node, TextAnchorNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount)
+ {
+ if (node == null) return;
+
+ Debug.Assert(node.parent == parentNode);
+
+ if (parentColor == RED) {
+ Debug.Assert(node.color == BLACK);
+ }
+ if (node.color == BLACK) {
+ blackCount++;
+ }
+ if (node.left == null && node.right == null) {
+ // node is a leaf node:
+ if (expectedBlackCount == -1)
+ expectedBlackCount = blackCount;
+ else
+ Debug.Assert(expectedBlackCount == blackCount);
+ }
+ CheckNodeProperties(node.left, node, node.color, blackCount, ref expectedBlackCount);
+ CheckNodeProperties(node.right, node, node.color, blackCount, ref expectedBlackCount);
+ }
+ #endif
+ #endregion
+
+ #region GetTreeAsString
+ #if DEBUG
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ public string GetTreeAsString()
+ {
+ if (root == null)
+ return "<empty tree>";
+ StringBuilder b = new StringBuilder();
+ AppendTreeToString(root, b, 0);
+ return b.ToString();
+ }
+
+ static void AppendTreeToString(TextAnchorNode node, StringBuilder b, int indent)
+ {
+ if (node.color == RED)
+ b.Append("RED ");
+ else
+ b.Append("BLACK ");
+ b.AppendLine(node.ToString());
+ indent += 2;
+ if (node.left != null) {
+ b.Append(' ', indent);
+ b.Append("L: ");
+ AppendTreeToString(node.left, b, indent);
+ }
+ if (node.right != null) {
+ b.Append(' ', indent);
+ b.Append("R: ");
+ AppendTreeToString(node.right, b, indent);
+ }
+ }
+ #endif
+ #endregion
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs
new file mode 100644
index 000000000..84fc86f44
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs
@@ -0,0 +1,836 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Linq;
+using System.Globalization;
+using System.Linq.Expressions;
+using System.Threading;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// This class is the main class of the text model. Basically, it is a <see cref="System.Text.StringBuilder"/> with events.
+ /// </summary>
+ /// <remarks>
+ /// <b>Thread safety:</b>
+ /// <inheritdoc cref="VerifyAccess"/>
+ /// <para>However, there is a single method that is thread-safe: <see cref="CreateSnapshot()"/> (and its overloads).</para>
+ /// </remarks>
+ public sealed class TextDocument : ITextSource, INotifyPropertyChanged
+ {
+ #region Thread ownership
+ readonly object lockObject = new object();
+ Thread owner = Thread.CurrentThread;
+
+ /// <summary>
+ /// Verifies that the current thread is the documents owner thread.
+ /// Throws an <see cref="InvalidOperationException"/> if the wrong thread accesses the TextDocument.
+ /// </summary>
+ /// <remarks>
+ /// <para>The TextDocument class is not thread-safe. A document instance expects to have a single owner thread
+ /// and will throw an <see cref="InvalidOperationException"/> when accessed from another thread.
+ /// It is possible to change the owner thread using the <see cref="SetOwnerThread"/> method.</para>
+ /// </remarks>
+ public void VerifyAccess()
+ {
+ if (Thread.CurrentThread != owner)
+ throw new InvalidOperationException("TextDocument can be accessed only from the thread that owns it.");
+ }
+
+ /// <summary>
+ /// Transfers ownership of the document to another thread. This method can be used to load
+ /// a file into a TextDocument on a background thread and then transfer ownership to the UI thread
+ /// for displaying the document.
+ /// </summary>
+ /// <remarks>
+ /// <inheritdoc cref="VerifyAccess"/>
+ /// <para>
+ /// The owner can be set to null, which means that no thread can access the document. But, if the document
+ /// has no owner thread, any thread may take ownership by calling <see cref="SetOwnerThread"/>.
+ /// </para>
+ /// </remarks>
+ public void SetOwnerThread(Thread newOwner)
+ {
+ // We need to lock here to ensure that in the null owner case,
+ // only one thread succeeds in taking ownership.
+ lock (lockObject) {
+ if (owner != null) {
+ VerifyAccess();
+ }
+ owner = newOwner;
+ }
+ }
+ #endregion
+
+ #region Fields + Constructor
+ readonly Rope<char> rope;
+ readonly DocumentLineTree lineTree;
+ readonly LineManager lineManager;
+ readonly TextAnchorTree anchorTree;
+ ChangeTrackingCheckpoint currentCheckpoint;
+
+ /// <summary>
+ /// Create an empty text document.
+ /// </summary>
+ public TextDocument()
+ : this(string.Empty)
+ {
+ }
+
+ /// <summary>
+ /// Create a new text document with the specified initial text.
+ /// </summary>
+ public TextDocument(IEnumerable<char> initialText)
+ {
+ if (initialText == null)
+ throw new ArgumentNullException("initialText");
+ rope = new Rope<char>(initialText);
+ lineTree = new DocumentLineTree(this);
+ lineManager = new LineManager(lineTree, this);
+ lineTrackers.CollectionChanged += delegate {
+ lineManager.UpdateListOfLineTrackers();
+ };
+
+ anchorTree = new TextAnchorTree(this);
+ undoStack = new UndoStack();
+ FireChangeEvents();
+ }
+
+ /// <summary>
+ /// Create a new text document with the specified initial text.
+ /// </summary>
+ public TextDocument(ITextSource initialText)
+ : this(GetTextFromTextSource(initialText))
+ {
+ }
+
+ // gets the text from a text source, directly retrieving the underlying rope where possible
+ static IEnumerable<char> GetTextFromTextSource(ITextSource textSource)
+ {
+ if (textSource == null)
+ throw new ArgumentNullException("textSource");
+
+ RopeTextSource rts = textSource as RopeTextSource;
+ if (rts != null)
+ return rts.GetRope();
+
+ TextDocument doc = textSource as TextDocument;
+ if (doc != null)
+ return doc.rope;
+
+ return textSource.Text;
+ }
+ #endregion
+
+ #region Text
+ void ThrowIfRangeInvalid(int offset, int length)
+ {
+ if (offset < 0 || offset > rope.Length) {
+ throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
+ }
+ if (length < 0 || offset + length > rope.Length) {
+ throw new ArgumentOutOfRangeException("length", length, "0 <= length, offset(" + offset + ")+length <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
+ }
+ }
+
+ /// <inheritdoc/>
+ public string GetText(int offset, int length)
+ {
+ VerifyAccess();
+ return rope.ToString(offset, length);
+ }
+
+ /// <summary>
+ /// Retrieves the text for a portion of the document.
+ /// </summary>
+ public string GetText(ISegment segment)
+ {
+ if (segment == null)
+ throw new ArgumentNullException("segment");
+ return GetText(segment.Offset, segment.Length);
+ }
+
+ int ITextSource.IndexOfAny(char[] anyOf, int startIndex, int count)
+ {
+ DebugVerifyAccess(); // frequently called (NewLineFinder), so must be fast in release builds
+ return rope.IndexOfAny(anyOf, startIndex, count);
+ }
+
+ /// <inheritdoc/>
+ public char GetCharAt(int offset)
+ {
+ DebugVerifyAccess(); // frequently called, so must be fast in release builds
+ return rope[offset];
+ }
+
+ WeakReference cachedText;
+
+ /// <summary>
+ /// Gets/Sets the text of the whole document.
+ /// </summary>
+ public string Text {
+ get {
+ VerifyAccess();
+ string completeText = cachedText != null ? (cachedText.Target as string) : null;
+ if (completeText == null) {
+ completeText = rope.ToString();
+ cachedText = new WeakReference(completeText);
+ }
+ return completeText;
+ }
+ set {
+ VerifyAccess();
+ if (value == null)
+ throw new ArgumentNullException("value");
+ Replace(0, rope.Length, value);
+ }
+ }
+
+ /// <inheritdoc/>
+ /// <remarks><inheritdoc cref="Changing"/></remarks>
+ public event EventHandler TextChanged;
+
+ /// <inheritdoc/>
+ public int TextLength {
+ get {
+ VerifyAccess();
+ return rope.Length;
+ }
+ }
+
+ /// <summary>
+ /// Is raised when the TextLength property changes.
+ /// </summary>
+ /// <remarks><inheritdoc cref="Changing"/></remarks>
+ [Obsolete("This event will be removed in a future version; use the PropertyChanged event instead")]
+ public event EventHandler TextLengthChanged;
+
+ /// <summary>
+ /// Is raised when one of the properties <see cref="Text"/>, <see cref="TextLength"/>, <see cref="LineCount"/>,
+ /// <see cref="UndoStack"/> changes.
+ /// </summary>
+ /// <remarks><inheritdoc cref="Changing"/></remarks>
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ /// <summary>
+ /// Is raised before the document changes.
+ /// </summary>
+ /// <remarks>
+ /// <para>Here is the order in which events are raised during a document update:</para>
+ /// <list type="bullet">
+ /// <item><description><b><see cref="BeginUpdate">BeginUpdate()</see></b></description>
+ /// <list type="bullet">
+ /// <item><description>Start of change group (on undo stack)</description></item>
+ /// <item><description><see cref="UpdateStarted"/> event is raised</description></item>
+ /// </list></item>
+ /// <item><description><b><see cref="Insert(int,string)">Insert()</see> / <see cref="Remove(int,int)">Remove()</see> / <see cref="Replace(int,int,string)">Replace()</see></b></description>
+ /// <list type="bullet">
+ /// <item><description><see cref="Changing"/> event is raised</description></item>
+ /// <item><description>The document is changed</description></item>
+ /// <item><description><see cref="TextAnchor.Deleted">TextAnchor.Deleted</see> event is raised if anchors were
+ /// in the deleted text portion</description></item>
+ /// <item><description><see cref="Changed"/> event is raised</description></item>
+ /// </list></item>
+ /// <item><description><b><see cref="EndUpdate">EndUpdate()</see></b></description>
+ /// <list type="bullet">
+ /// <item><description><see cref="TextChanged"/> event is raised</description></item>
+ /// <item><description><see cref="PropertyChanged"/> event is raised (for the Text, TextLength, LineCount properties, in that order)</description></item>
+ /// <item><description>End of change group (on undo stack)</description></item>
+ /// <item><description><see cref="UpdateFinished"/> event is raised</description></item>
+ /// </list></item>
+ /// </list>
+ /// <para>
+ /// If the insert/remove/replace methods are called without a call to <c>BeginUpdate()</c>,
+ /// they will call <c>BeginUpdate()</c> and <c>EndUpdate()</c> to ensure no change happens outside of <c>UpdateStarted</c>/<c>UpdateFinished</c>.
+ /// </para><para>
+ /// There can be multiple document changes between the <c>BeginUpdate()</c> and <c>EndUpdate()</c> calls.
+ /// In this case, the events associated with EndUpdate will be raised only once after the whole document update is done.
+ /// </para><para>
+ /// The <see cref="UndoStack"/> listens to the <c>UpdateStarted</c> and <c>UpdateFinished</c> events to group all changes into a single undo step.
+ /// </para>
+ /// </remarks>
+ public event EventHandler<DocumentChangeEventArgs> Changing;
+
+ /// <summary>
+ /// Is raised after the document has changed.
+ /// </summary>
+ /// <remarks><inheritdoc cref="Changing"/></remarks>
+ public event EventHandler<DocumentChangeEventArgs> Changed;
+
+ /// <summary>
+ /// Creates a snapshot of the current text.
+ /// </summary>
+ /// <remarks>
+ /// <para>This method returns an immutable snapshot of the document, and may be safely called even when
+ /// the document's owner thread is concurrently modifying the document.
+ /// </para><para>
+ /// This special thread-safety guarantee is valid only for TextDocument.CreateSnapshot(), not necessarily for other
+ /// classes implementing ITextSource.CreateSnapshot().
+ /// </para><para>
+ /// </para>
+ /// </remarks>
+ public ITextSource CreateSnapshot()
+ {
+ lock (lockObject) {
+ return new RopeTextSource(rope.Clone());
+ }
+ }
+
+ /// <summary>
+ /// Creates a snapshot of the current text.
+ /// Additionally, creates a checkpoint that allows tracking document changes.
+ /// </summary>
+ /// <remarks><inheritdoc cref="CreateSnapshot()"/><inheritdoc cref="ChangeTrackingCheckpoint"/></remarks>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", Justification = "Need to return snapshot and checkpoint together to ensure thread-safety")]
+ public ITextSource CreateSnapshot(out ChangeTrackingCheckpoint checkpoint)
+ {
+ lock (lockObject) {
+ if (currentCheckpoint == null)
+ currentCheckpoint = new ChangeTrackingCheckpoint(lockObject);
+ checkpoint = currentCheckpoint;
+ return new RopeTextSource(rope.Clone());
+ }
+ }
+
+ internal ChangeTrackingCheckpoint CreateChangeTrackingCheckpoint()
+ {
+ lock (lockObject) {
+ if (currentCheckpoint == null)
+ currentCheckpoint = new ChangeTrackingCheckpoint(lockObject);
+ return currentCheckpoint;
+ }
+ }
+
+ /// <summary>
+ /// Creates a snapshot of a part of the current text.
+ /// </summary>
+ /// <remarks><inheritdoc cref="CreateSnapshot()"/></remarks>
+ public ITextSource CreateSnapshot(int offset, int length)
+ {
+ lock (lockObject) {
+ return new RopeTextSource(rope.GetRange(offset, length));
+ }
+ }
+
+ /// <inheritdoc/>
+ public System.IO.TextReader CreateReader()
+ {
+ lock (lockObject) {
+ return new RopeTextReader(rope);
+ }
+ }
+ #endregion
+
+ #region BeginUpdate / EndUpdate
+ int beginUpdateCount;
+
+ /// <summary>
+ /// Gets if an update is running.
+ /// </summary>
+ /// <remarks><inheritdoc cref="BeginUpdate"/></remarks>
+ public bool IsInUpdate {
+ get {
+ VerifyAccess();
+ return beginUpdateCount > 0;
+ }
+ }
+
+ /// <summary>
+ /// Immediately calls <see cref="BeginUpdate()"/>,
+ /// and returns an IDisposable that calls <see cref="EndUpdate()"/>.
+ /// </summary>
+ /// <remarks><inheritdoc cref="BeginUpdate"/></remarks>
+ public IDisposable RunUpdate()
+ {
+ BeginUpdate();
+ return new CallbackOnDispose(EndUpdate);
+ }
+
+ /// <summary>
+ /// <para>Begins a group of document changes.</para>
+ /// <para>Some events are suspended until EndUpdate is called, and the <see cref="UndoStack"/> will
+ /// group all changes into a single action.</para>
+ /// <para>Calling BeginUpdate several times increments a counter, only after the appropriate number
+ /// of EndUpdate calls the events resume their work.</para>
+ /// </summary>
+ /// <remarks><inheritdoc cref="Changing"/></remarks>
+ public void BeginUpdate()
+ {
+ VerifyAccess();
+ if (inDocumentChanging)
+ throw new InvalidOperationException("Cannot change document within another document change.");
+ beginUpdateCount++;
+ if (beginUpdateCount == 1) {
+ undoStack.StartUndoGroup();
+ if (UpdateStarted != null)
+ UpdateStarted(this, EventArgs.Empty);
+ }
+ }
+
+ /// <summary>
+ /// Ends a group of document changes.
+ /// </summary>
+ /// <remarks><inheritdoc cref="Changing"/></remarks>
+ public void EndUpdate()
+ {
+ VerifyAccess();
+ if (inDocumentChanging)
+ throw new InvalidOperationException("Cannot end update within document change.");
+ if (beginUpdateCount == 0)
+ throw new InvalidOperationException("No update is active.");
+ if (beginUpdateCount == 1) {
+ // fire change events inside the change group - event handlers might add additional
+ // document changes to the change group
+ FireChangeEvents();
+ undoStack.EndUndoGroup();
+ beginUpdateCount = 0;
+ if (UpdateFinished != null)
+ UpdateFinished(this, EventArgs.Empty);
+ } else {
+ beginUpdateCount -= 1;
+ }
+ }
+
+ /// <summary>
+ /// Occurs when a document change starts.
+ /// </summary>
+ /// <remarks><inheritdoc cref="Changing"/></remarks>
+ public event EventHandler UpdateStarted;
+
+ /// <summary>
+ /// Occurs when a document change is finished.
+ /// </summary>
+ /// <remarks><inheritdoc cref="Changing"/></remarks>
+ public event EventHandler UpdateFinished;
+ #endregion
+
+ #region Fire events after update
+ int oldTextLength;
+ int oldLineCount;
+ bool fireTextChanged;
+
+ /// <summary>
+ /// Fires TextChanged, TextLengthChanged, LineCountChanged if required.
+ /// </summary>
+ internal void FireChangeEvents()
+ {
+ // it may be necessary to fire the event multiple times if the document is changed
+ // from inside the event handlers
+ while (fireTextChanged) {
+ fireTextChanged = false;
+ if (TextChanged != null)
+ TextChanged(this, EventArgs.Empty);
+ OnPropertyChanged("Text");
+
+ int textLength = rope.Length;
+ if (textLength != oldTextLength) {
+ oldTextLength = textLength;
+ if (TextLengthChanged != null)
+ TextLengthChanged(this, EventArgs.Empty);
+ OnPropertyChanged("TextLength");
+ }
+ int lineCount = lineTree.LineCount;
+ if (lineCount != oldLineCount) {
+ oldLineCount = lineCount;
+ if (LineCountChanged != null)
+ LineCountChanged(this, EventArgs.Empty);
+ OnPropertyChanged("LineCount");
+ }
+ }
+ }
+
+ void OnPropertyChanged(string propertyName)
+ {
+ if (PropertyChanged != null)
+ PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
+ }
+ #endregion
+
+ #region Insert / Remove / Replace
+ /// <summary>
+ /// Inserts text.
+ /// </summary>
+ public void Insert(int offset, string text)
+ {
+ Replace(offset, 0, text);
+ }
+
+ /// <summary>
+ /// Removes text.
+ /// </summary>
+ public void Remove(ISegment segment)
+ {
+ Replace(segment, string.Empty);
+ }
+
+ /// <summary>
+ /// Removes text.
+ /// </summary>
+ public void Remove(int offset, int length)
+ {
+ Replace(offset, length, string.Empty);
+ }
+
+ internal bool inDocumentChanging;
+
+ /// <summary>
+ /// Replaces text.
+ /// </summary>
+ public void Replace(ISegment segment, string text)
+ {
+ if (segment == null)
+ throw new ArgumentNullException("segment");
+ Replace(segment.Offset, segment.Length, text, null);
+ }
+
+ /// <summary>
+ /// Replaces text.
+ /// </summary>
+ public void Replace(int offset, int length, string text)
+ {
+ Replace(offset, length, text, null);
+ }
+
+ /// <summary>
+ /// Replaces text.
+ /// </summary>
+ /// <param name="offset">The starting offset of the text to be replaced.</param>
+ /// <param name="length">The length of the text to be replaced.</param>
+ /// <param name="text">The new text.</param>
+ /// <param name="offsetChangeMappingType">The offsetChangeMappingType determines how offsets inside the old text are mapped to the new text.
+ /// This affects how the anchors and segments inside the replaced region behave.</param>
+ public void Replace(int offset, int length, string text, OffsetChangeMappingType offsetChangeMappingType)
+ {
+ if (text == null)
+ throw new ArgumentNullException("text");
+ // Please see OffsetChangeMappingType XML comments for details on how these modes work.
+ switch (offsetChangeMappingType) {
+ case OffsetChangeMappingType.Normal:
+ Replace(offset, length, text, null);
+ break;
+ case OffsetChangeMappingType.KeepAnchorBeforeInsertion:
+ Replace(offset, length, text, OffsetChangeMap.FromSingleElement(
+ new OffsetChangeMapEntry(offset, length, text.Length, false, true)));
+ break;
+ case OffsetChangeMappingType.RemoveAndInsert:
+ if (length == 0 || text.Length == 0) {
+ // only insertion or only removal?
+ // OffsetChangeMappingType doesn't matter, just use Normal.
+ Replace(offset, length, text, null);
+ } else {
+ OffsetChangeMap map = new OffsetChangeMap(2);
+ map.Add(new OffsetChangeMapEntry(offset, length, 0));
+ map.Add(new OffsetChangeMapEntry(offset, 0, text.Length));
+ map.Freeze();
+ Replace(offset, length, text, map);
+ }
+ break;
+ case OffsetChangeMappingType.CharacterReplace:
+ if (length == 0 || text.Length == 0) {
+ // only insertion or only removal?
+ // OffsetChangeMappingType doesn't matter, just use Normal.
+ Replace(offset, length, text, null);
+ } else if (text.Length > length) {
+ // look at OffsetChangeMappingType.CharacterReplace XML comments on why we need to replace
+ // the last character
+ OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + length - 1, 1, 1 + text.Length - length);
+ Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry));
+ } else if (text.Length < length) {
+ OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + text.Length, length - text.Length, 0, true, false);
+ Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry));
+ } else {
+ Replace(offset, length, text, OffsetChangeMap.Empty);
+ }
+ break;
+ default:
+ throw new ArgumentOutOfRangeException("offsetChangeMappingType", offsetChangeMappingType, "Invalid enum value");
+ }
+ }
+
+ /// <summary>
+ /// Replaces text.
+ /// </summary>
+ /// <param name="offset">The starting offset of the text to be replaced.</param>
+ /// <param name="length">The length of the text to be replaced.</param>
+ /// <param name="text">The new text.</param>
+ /// <param name="offsetChangeMap">The offsetChangeMap determines how offsets inside the old text are mapped to the new text.
+ /// This affects how the anchors and segments inside the replaced region behave.
+ /// If you pass null (the default when using one of the other overloads), the offsets are changed as
+ /// in OffsetChangeMappingType.Normal mode.
+ /// If you pass OffsetChangeMap.Empty, then everything will stay in its old place (OffsetChangeMappingType.CharacterReplace mode).
+ /// The offsetChangeMap must be a valid 'explanation' for the document change. See <see cref="OffsetChangeMap.IsValidForDocumentChange"/>.
+ /// Passing an OffsetChangeMap to the Replace method will automatically freeze it to ensure the thread safety of the resulting
+ /// DocumentChangeEventArgs instance.
+ /// </param>
+ public void Replace(int offset, int length, string text, OffsetChangeMap offsetChangeMap)
+ {
+ if (text == null)
+ throw new ArgumentNullException("text");
+
+ if (offsetChangeMap != null)
+ offsetChangeMap.Freeze();
+
+ // Ensure that all changes take place inside an update group.
+ // Will also take care of throwing an exception if inDocumentChanging is set.
+ BeginUpdate();
+ try {
+ // protect document change against corruption by other changes inside the event handlers
+ inDocumentChanging = true;
+ try {
+ // The range verification must wait until after the BeginUpdate() call because the document
+ // might be modified inside the UpdateStarted event.
+ ThrowIfRangeInvalid(offset, length);
+
+ DoReplace(offset, length, text, offsetChangeMap);
+ } finally {
+ inDocumentChanging = false;
+ }
+ } finally {
+ EndUpdate();
+ }
+ }
+
+ void DoReplace(int offset, int length, string newText, OffsetChangeMap offsetChangeMap)
+ {
+ if (length == 0 && newText.Length == 0)
+ return;
+
+ // trying to replace a single character in 'Normal' mode?
+ // for single characters, 'CharacterReplace' mode is equivalent, but more performant
+ // (we don't have to touch the anchorTree at all in 'CharacterReplace' mode)
+ if (length == 1 && newText.Length == 1 && offsetChangeMap == null)
+ offsetChangeMap = OffsetChangeMap.Empty;
+
+ string removedText = rope.ToString(offset, length);
+ DocumentChangeEventArgs args = new DocumentChangeEventArgs(offset, removedText, newText, offsetChangeMap);
+
+ // fire DocumentChanging event
+ if (Changing != null)
+ Changing(this, args);
+
+ undoStack.Push(this, args);
+
+ cachedText = null; // reset cache of complete document text
+ fireTextChanged = true;
+ DelayedEvents delayedEvents = new DelayedEvents();
+
+ lock (lockObject) {
+ // create linked list of checkpoints, if required
+ if (currentCheckpoint != null) {
+ currentCheckpoint = currentCheckpoint.Append(args);
+ }
+
+ // now update the textBuffer and lineTree
+ if (offset == 0 && length == rope.Length) {
+ // optimize replacing the whole document
+ rope.Clear();
+ rope.InsertText(0, newText);
+ lineManager.Rebuild();
+ } else {
+ rope.RemoveRange(offset, length);
+ lineManager.Remove(offset, length);
+ #if DEBUG
+ lineTree.CheckProperties();
+ #endif
+ rope.InsertText(offset, newText);
+ lineManager.Insert(offset, newText);
+ #if DEBUG
+ lineTree.CheckProperties();
+ #endif
+ }
+ }
+
+ // update text anchors
+ if (offsetChangeMap == null) {
+ anchorTree.HandleTextChange(args.CreateSingleChangeMapEntry(), delayedEvents);
+ } else {
+ foreach (OffsetChangeMapEntry entry in offsetChangeMap) {
+ anchorTree.HandleTextChange(entry, delayedEvents);
+ }
+ }
+
+ // raise delayed events after our data structures are consistent again
+ delayedEvents.RaiseEvents();
+
+ // fire DocumentChanged event
+ if (Changed != null)
+ Changed(this, args);
+ }
+ #endregion
+
+ #region GetLineBy...
+ /// <summary>
+ /// Gets a read-only list of lines.
+ /// </summary>
+ /// <remarks><inheritdoc cref="DocumentLine"/></remarks>
+ public IList<DocumentLine> Lines {
+ get { return lineTree; }
+ }
+
+ /// <summary>
+ /// Gets a line by the line number: O(log n)
+ /// </summary>
+ public DocumentLine GetLineByNumber(int number)
+ {
+ VerifyAccess();
+ if (number < 1 || number > lineTree.LineCount)
+ throw new ArgumentOutOfRangeException("number", number, "Value must be between 1 and " + lineTree.LineCount);
+ return lineTree.GetByNumber(number);
+ }
+
+ /// <summary>
+ /// Gets a document lines by offset.
+ /// Runtime: O(log n)
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.Int32.ToString")]
+ public DocumentLine GetLineByOffset(int offset)
+ {
+ VerifyAccess();
+ if (offset < 0 || offset > rope.Length) {
+ throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString());
+ }
+ return lineTree.GetByOffset(offset);
+ }
+ #endregion
+
+ /// <summary>
+ /// Gets the offset from a text location.
+ /// </summary>
+ /// <seealso cref="GetLocation"/>
+ public int GetOffset(TextLocation location)
+ {
+ return GetOffset(location.Line, location.Column);
+ }
+
+ /// <summary>
+ /// Gets the offset from a text location.
+ /// </summary>
+ /// <seealso cref="GetLocation"/>
+ public int GetOffset(int line, int column)
+ {
+ DocumentLine docLine = GetLineByNumber(line);
+ if (column <= 0)
+ return docLine.Offset;
+ if (column > docLine.Length)
+ return docLine.EndOffset;
+ return docLine.Offset + column - 1;
+ }
+
+ /// <summary>
+ /// Gets the location from an offset.
+ /// </summary>
+ /// <seealso cref="GetOffset(TextLocation)"/>
+ public TextLocation GetLocation(int offset)
+ {
+ DocumentLine line = GetLineByOffset(offset);
+ return new TextLocation(line.LineNumber, offset - line.Offset + 1);
+ }
+
+ readonly ObservableCollection<ILineTracker> lineTrackers = new ObservableCollection<ILineTracker>();
+
+ /// <summary>
+ /// Gets the list of <see cref="ILineTracker"/>s attached to this document.
+ /// You can add custom line trackers to this list.
+ /// </summary>
+ public IList<ILineTracker> LineTrackers {
+ get {
+ VerifyAccess();
+ return lineTrackers;
+ }
+ }
+
+ UndoStack undoStack;
+
+ /// <summary>
+ /// Gets the <see cref="UndoStack"/> of the document.
+ /// </summary>
+ /// <remarks>This property can also be used to set the undo stack, e.g. for sharing a common undo stack between multiple documents.</remarks>
+ public UndoStack UndoStack {
+ get { return undoStack; }
+ set {
+ if (value == null)
+ throw new ArgumentNullException();
+ if (value != undoStack) {
+ undoStack.ClearAll(); // first clear old undo stack, so that it can't be used to perform unexpected changes on this document
+ // ClearAll() will also throw an exception when it's not safe to replace the undo stack (e.g. update is currently in progress)
+ undoStack = value;
+ OnPropertyChanged("UndoStack");
+ }
+ }
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="TextAnchor"/> at the specified offset.
+ /// </summary>
+ /// <inheritdoc cref="TextAnchor" select="remarks|example"/>
+ public TextAnchor CreateAnchor(int offset)
+ {
+ VerifyAccess();
+ if (offset < 0 || offset > rope.Length) {
+ throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
+ }
+ return anchorTree.CreateAnchor(offset);
+ }
+
+ #region LineCount
+ /// <summary>
+ /// Gets the total number of lines in the document.
+ /// Runtime: O(1).
+ /// </summary>
+ public int LineCount {
+ get {
+ VerifyAccess();
+ return lineTree.LineCount;
+ }
+ }
+
+ /// <summary>
+ /// Is raised when the LineCount property changes.
+ /// </summary>
+ [Obsolete("This event will be removed in a future version; use the PropertyChanged event instead")]
+ public event EventHandler LineCountChanged;
+ #endregion
+
+ #region Debugging
+ [Conditional("DEBUG")]
+ internal void DebugVerifyAccess()
+ {
+ VerifyAccess();
+ }
+
+ /// <summary>
+ /// Gets the document lines tree in string form.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
+ internal string GetLineTreeAsString()
+ {
+ #if DEBUG
+ return lineTree.GetTreeAsString();
+ #else
+ return "Not available in release build.";
+ #endif
+ }
+
+ /// <summary>
+ /// Gets the text anchor tree in string form.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
+ internal string GetTextAnchorTreeAsString()
+ {
+ #if DEBUG
+ return anchorTree.GetTreeAsString();
+ #else
+ return "Not available in release build.";
+ #endif
+ }
+ #endregion
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocumentWeakEventManager.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocumentWeakEventManager.cs
new file mode 100644
index 000000000..de304b722
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocumentWeakEventManager.cs
@@ -0,0 +1,149 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Contains weak event managers for the TextDocument events.
+ /// </summary>
+ public static class TextDocumentWeakEventManager
+ {
+ /// <summary>
+ /// Weak event manager for the <see cref="TextDocument.UpdateStarted"/> event.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
+ public sealed class UpdateStarted : WeakEventManagerBase<UpdateStarted, TextDocument>
+ {
+ /// <inheritdoc/>
+ protected override void StartListening(TextDocument source)
+ {
+ source.UpdateStarted += DeliverEvent;
+ }
+
+ /// <inheritdoc/>
+ protected override void StopListening(TextDocument source)
+ {
+ source.UpdateStarted -= DeliverEvent;
+ }
+ }
+
+ /// <summary>
+ /// Weak event manager for the <see cref="TextDocument.UpdateFinished"/> event.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
+ public sealed class UpdateFinished : WeakEventManagerBase<UpdateFinished, TextDocument>
+ {
+ /// <inheritdoc/>
+ protected override void StartListening(TextDocument source)
+ {
+ source.UpdateFinished += DeliverEvent;
+ }
+
+ /// <inheritdoc/>
+ protected override void StopListening(TextDocument source)
+ {
+ source.UpdateFinished -= DeliverEvent;
+ }
+ }
+
+ /// <summary>
+ /// Weak event manager for the <see cref="TextDocument.Changing"/> event.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
+ public sealed class Changing : WeakEventManagerBase<Changing, TextDocument>
+ {
+ /// <inheritdoc/>
+ protected override void StartListening(TextDocument source)
+ {
+ source.Changing += DeliverEvent;
+ }
+
+ /// <inheritdoc/>
+ protected override void StopListening(TextDocument source)
+ {
+ source.Changing -= DeliverEvent;
+ }
+ }
+
+ /// <summary>
+ /// Weak event manager for the <see cref="TextDocument.Changed"/> event.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
+ public sealed class Changed : WeakEventManagerBase<Changed, TextDocument>
+ {
+ /// <inheritdoc/>
+ protected override void StartListening(TextDocument source)
+ {
+ source.Changed += DeliverEvent;
+ }
+
+ /// <inheritdoc/>
+ protected override void StopListening(TextDocument source)
+ {
+ source.Changed -= DeliverEvent;
+ }
+ }
+
+ /// <summary>
+ /// Weak event manager for the <see cref="TextDocument.LineCountChanged"/> event.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
+ [Obsolete("The TextDocument.LineCountChanged event will be removed in a future version. Use PropertyChangedEventManager instead.")]
+ public sealed class LineCountChanged : WeakEventManagerBase<LineCountChanged, TextDocument>
+ {
+ /// <inheritdoc/>
+ protected override void StartListening(TextDocument source)
+ {
+ source.LineCountChanged += DeliverEvent;
+ }
+
+ /// <inheritdoc/>
+ protected override void StopListening(TextDocument source)
+ {
+ source.LineCountChanged -= DeliverEvent;
+ }
+ }
+
+ /// <summary>
+ /// Weak event manager for the <see cref="TextDocument.TextLengthChanged"/> event.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
+ [Obsolete("The TextDocument.TextLengthChanged event will be removed in a future version. Use PropertyChangedEventManager instead.")]
+ public sealed class TextLengthChanged : WeakEventManagerBase<TextLengthChanged, TextDocument>
+ {
+ /// <inheritdoc/>
+ protected override void StartListening(TextDocument source)
+ {
+ source.TextLengthChanged += DeliverEvent;
+ }
+
+ /// <inheritdoc/>
+ protected override void StopListening(TextDocument source)
+ {
+ source.TextLengthChanged -= DeliverEvent;
+ }
+ }
+
+ /// <summary>
+ /// Weak event manager for the <see cref="TextDocument.TextChanged"/> event.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
+ public sealed class TextChanged : WeakEventManagerBase<TextChanged, TextDocument>
+ {
+ /// <inheritdoc/>
+ protected override void StartListening(TextDocument source)
+ {
+ source.TextChanged += DeliverEvent;
+ }
+
+ /// <inheritdoc/>
+ protected override void StopListening(TextDocument source)
+ {
+ source.TextChanged -= DeliverEvent;
+ }
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextLocation.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextLocation.cs
new file mode 100644
index 000000000..1d3c2564c
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextLocation.cs
@@ -0,0 +1,166 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Globalization;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// A line/column position.
+ /// Text editor lines/columns are counted started from one.
+ /// </summary>
+ /// <remarks>
+ /// The document provides the methods <see cref="TextDocument.GetLocation"/> and
+ /// <see cref="TextDocument.GetOffset(TextLocation)"/> to convert between offsets and TextLocations.
+ /// </remarks>
+ public struct TextLocation : IComparable<TextLocation>, IEquatable<TextLocation>
+ {
+ /// <summary>
+ /// Represents no text location (0, 0).
+ /// </summary>
+ public static readonly TextLocation Empty = new TextLocation(0, 0);
+
+ /// <summary>
+ /// Creates a TextLocation instance.
+ /// <para>
+ /// Warning: the parameters are (line, column).
+ /// Not (column, line) as in ICSharpCode.TextEditor!
+ /// </para>
+ /// </summary>
+ public TextLocation(int line, int column)
+ {
+ y = line;
+ x = column;
+ }
+
+ int x, y;
+
+ /// <summary>
+ /// Gets the line number.
+ /// </summary>
+ public int Line {
+ get { return y; }
+ }
+
+ /// <summary>
+ /// Gets the column number.
+ /// </summary>
+ public int Column {
+ get { return x; }
+ }
+
+ /// <summary>
+ /// Gets whether the TextLocation instance is empty.
+ /// </summary>
+ public bool IsEmpty {
+ get {
+ return x <= 0 && y <= 0;
+ }
+ }
+
+ /// <summary>
+ /// Gets a string representation for debugging purposes.
+ /// </summary>
+ public override string ToString()
+ {
+ return string.Format(CultureInfo.InvariantCulture, "(Line {1}, Col {0})", this.x, this.y);
+ }
+
+ /// <summary>
+ /// Gets a hash code.
+ /// </summary>
+ public override int GetHashCode()
+ {
+ return unchecked (87 * x.GetHashCode() ^ y.GetHashCode());
+ }
+
+ /// <summary>
+ /// Equality test.
+ /// </summary>
+ public override bool Equals(object obj)
+ {
+ if (!(obj is TextLocation)) return false;
+ return (TextLocation)obj == this;
+ }
+
+ /// <summary>
+ /// Equality test.
+ /// </summary>
+ public bool Equals(TextLocation other)
+ {
+ return this == other;
+ }
+
+ /// <summary>
+ /// Equality test.
+ /// </summary>
+ public static bool operator ==(TextLocation left, TextLocation right)
+ {
+ return left.x == right.x && left.y == right.y;
+ }
+
+ /// <summary>
+ /// Inequality test.
+ /// </summary>
+ public static bool operator !=(TextLocation left, TextLocation right)
+ {
+ return left.x != right.x || left.y != right.y;
+ }
+
+ /// <summary>
+ /// Compares two text locations.
+ /// </summary>
+ public static bool operator <(TextLocation left, TextLocation right)
+ {
+ if (left.y < right.y)
+ return true;
+ else if (left.y == right.y)
+ return left.x < right.x;
+ else
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two text locations.
+ /// </summary>
+ public static bool operator >(TextLocation left, TextLocation right)
+ {
+ if (left.y > right.y)
+ return true;
+ else if (left.y == right.y)
+ return left.x > right.x;
+ else
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two text locations.
+ /// </summary>
+ public static bool operator <=(TextLocation left, TextLocation right)
+ {
+ return !(left > right);
+ }
+
+ /// <summary>
+ /// Compares two text locations.
+ /// </summary>
+ public static bool operator >=(TextLocation left, TextLocation right)
+ {
+ return !(left < right);
+ }
+
+ /// <summary>
+ /// Compares two text locations.
+ /// </summary>
+ public int CompareTo(TextLocation other)
+ {
+ if (this == other)
+ return 0;
+ if (this < other)
+ return -1;
+ else
+ return 1;
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextSegment.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextSegment.cs
new file mode 100644
index 000000000..d1834c214
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextSegment.cs
@@ -0,0 +1,248 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Diagnostics;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// A segment that can be put into a <see cref="TextSegmentCollection{T}"/>.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// A <see cref="TextSegment"/> can be stand-alone or part of a <see cref="TextSegmentCollection{T}"/>.
+ /// If the segment is stored inside a TextSegmentCollection, its Offset and Length will be updated by that collection.
+ /// </para>
+ /// <para>
+ /// When the document changes, the offsets of all text segments in the TextSegmentCollection will be adjusted accordingly.
+ /// Start offsets move like <see cref="AnchorMovementType">AnchorMovementType.AfterInsertion</see>,
+ /// end offsets move like <see cref="AnchorMovementType">AnchorMovementType.BeforeInsertion</see>
+ /// (i.e. the segment will always stay as small as possible).</para>
+ /// <para>
+ /// If a document change causes a segment to be deleted completely, it will be reduced to length 0, but segments are
+ /// never automatically removed from the collection.
+ /// Segments with length 0 will never expand due to document changes, and they move as <c>AfterInsertion</c>.
+ /// </para>
+ /// <para>
+ /// Thread-safety: a TextSegmentCollection that is connected to a <see cref="TextDocument"/> may only be used on that document's owner thread.
+ /// A disconnected TextSegmentCollection is safe for concurrent reads, but concurrent access is not safe when there are writes.
+ /// Keep in mind that reading the Offset properties of a text segment inside the collection is a read access on the
+ /// collection; and setting an Offset property of a text segment is a write access on the collection.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="ISegment"/>
+ /// <seealso cref="AnchorSegment"/>
+ /// <seealso cref="TextSegmentCollection{T}"/>
+ public class TextSegment : ISegment
+ {
+ internal ISegmentTree ownerTree;
+ internal TextSegment left, right, parent;
+
+ /// <summary>
+ /// The color of the segment in the red/black tree.
+ /// </summary>
+ internal bool color;
+
+ /// <summary>
+ /// The "length" of the node (distance to previous node)
+ /// </summary>
+ internal int nodeLength;
+
+ /// <summary>
+ /// The total "length" of this subtree.
+ /// </summary>
+ internal int totalNodeLength; // totalNodeLength = nodeLength + left.totalNodeLength + right.totalNodeLength
+
+ /// <summary>
+ /// The length of the segment (do not confuse with nodeLength).
+ /// </summary>
+ internal int segmentLength;
+
+ /// <summary>
+ /// distanceToMaxEnd = Max(segmentLength,
+ /// left.distanceToMaxEnd + left.Offset - Offset,
+ /// left.distanceToMaxEnd + right.Offset - Offset)
+ /// </summary>
+ internal int distanceToMaxEnd;
+
+ int ISegment.Offset {
+ get { return StartOffset; }
+ }
+
+ /// <summary>
+ /// Gets whether this segment is connected to a TextSegmentCollection and will automatically
+ /// update its offsets.
+ /// </summary>
+ protected bool IsConnectedToCollection {
+ get {
+ return ownerTree != null;
+ }
+ }
+
+ /// <summary>
+ /// Gets/Sets the start offset of the segment.
+ /// </summary>
+ /// <remarks>
+ /// When setting the start offset, the end offset will change, too: the Length of the segment will stay constant.
+ /// </remarks>
+ public int StartOffset {
+ get {
+ // If the segment is not connected to a tree, we store the offset in "nodeLength".
+ // Otherwise, "nodeLength" contains the distance to the start offset of the previous node
+ Debug.Assert(!(ownerTree == null && parent != null));
+ Debug.Assert(!(ownerTree == null && left != null));
+
+ TextSegment n = this;
+ int offset = n.nodeLength;
+ if (n.left != null)
+ offset += n.left.totalNodeLength;
+ while (n.parent != null) {
+ if (n == n.parent.right) {
+ if (n.parent.left != null)
+ offset += n.parent.left.totalNodeLength;
+ offset += n.parent.nodeLength;
+ }
+ n = n.parent;
+ }
+ return offset;
+ }
+ set {
+ if (value < 0)
+ throw new ArgumentOutOfRangeException("value", "Offset must not be negative");
+ if (this.StartOffset != value) {
+ // need a copy of the variable because ownerTree.Remove() sets this.ownerTree to null
+ ISegmentTree ownerTree = this.ownerTree;
+ if (ownerTree != null) {
+ ownerTree.Remove(this);
+ nodeLength = value;
+ ownerTree.Add(this);
+ } else {
+ nodeLength = value;
+ }
+ OnSegmentChanged();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets/Sets the end offset of the segment.
+ /// </summary>
+ /// <remarks>
+ /// Setting the end offset will change the length, the start offset will stay constant.
+ /// </remarks>
+ public int EndOffset {
+ get {
+ return StartOffset + Length;
+ }
+ set {
+ int newLength = value - StartOffset;
+ if (newLength < 0)
+ throw new ArgumentOutOfRangeException("value", "EndOffset must be greater or equal to StartOffset");
+ Length = newLength;
+ }
+ }
+
+ /// <summary>
+ /// Gets/Sets the length of the segment.
+ /// </summary>
+ /// <remarks>
+ /// Setting the length will change the end offset, the start offset will stay constant.
+ /// </remarks>
+ public int Length {
+ get {
+ return segmentLength;
+ }
+ set {
+ if (value < 0)
+ throw new ArgumentOutOfRangeException("value", "Length must not be negative");
+ if (segmentLength != value) {
+ segmentLength = value;
+ if (ownerTree != null)
+ ownerTree.UpdateAugmentedData(this);
+ OnSegmentChanged();
+ }
+ }
+ }
+
+ /// <summary>
+ /// This method gets called when the StartOffset/Length/EndOffset properties are set.
+ /// It is not called when StartOffset/Length/EndOffset change due to document changes
+ /// </summary>
+ protected virtual void OnSegmentChanged()
+ {
+ }
+
+ internal TextSegment LeftMost {
+ get {
+ TextSegment node = this;
+ while (node.left != null)
+ node = node.left;
+ return node;
+ }
+ }
+
+ internal TextSegment RightMost {
+ get {
+ TextSegment node = this;
+ while (node.right != null)
+ node = node.right;
+ return node;
+ }
+ }
+
+ /// <summary>
+ /// Gets the inorder successor of the node.
+ /// </summary>
+ internal TextSegment Successor {
+ get {
+ if (right != null) {
+ return right.LeftMost;
+ } else {
+ TextSegment node = this;
+ TextSegment oldNode;
+ do {
+ oldNode = node;
+ node = node.parent;
+ // go up until we are coming out of a left subtree
+ } while (node != null && node.right == oldNode);
+ return node;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the inorder predecessor of the node.
+ /// </summary>
+ internal TextSegment Predecessor {
+ get {
+ if (left != null) {
+ return left.RightMost;
+ } else {
+ TextSegment node = this;
+ TextSegment oldNode;
+ do {
+ oldNode = node;
+ node = node.parent;
+ // go up until we are coming out of a right subtree
+ } while (node != null && node.left == oldNode);
+ return node;
+ }
+ }
+ }
+
+ #if DEBUG
+ internal string ToDebugString()
+ {
+ return "[nodeLength=" + nodeLength + " totalNodeLength=" + totalNodeLength
+ + " distanceToMaxEnd=" + distanceToMaxEnd + " MaxEndOffset=" + (StartOffset + distanceToMaxEnd) + "]";
+ }
+ #endif
+
+ /// <inheritdoc/>
+ public override string ToString()
+ {
+ return "[" + GetType().Name + " Offset=" + StartOffset + " Length=" + Length + " EndOffset=" + EndOffset + "]";
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextSegmentCollection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextSegmentCollection.cs
new file mode 100644
index 000000000..549dbb77d
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextSegmentCollection.cs
@@ -0,0 +1,971 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using Tango.Scripting.Editors.Utils;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Windows;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Interface to allow TextSegments to access the TextSegmentCollection - we cannot use a direct reference
+ /// because TextSegmentCollection is generic.
+ /// </summary>
+ interface ISegmentTree
+ {
+ void Add(TextSegment s);
+ void Remove(TextSegment s);
+ void UpdateAugmentedData(TextSegment s);
+ }
+
+ /// <summary>
+ /// <para>
+ /// A collection of text segments that supports efficient lookup of segments
+ /// intersecting with another segment.
+ /// </para>
+ /// </summary>
+ /// <remarks><inheritdoc cref="TextSegment"/></remarks>
+ /// <see cref="TextSegment"/>
+ public sealed class TextSegmentCollection<T> : ICollection<T>, ISegmentTree, IWeakEventListener where T : TextSegment
+ {
+ // Implementation: this is basically a mixture of an augmented interval tree
+ // and the TextAnchorTree.
+
+ // WARNING: you need to understand interval trees (the version with the augmented 'high'/'max' field)
+ // and how the TextAnchorTree works before you have any chance of understanding this code.
+
+ // This means that every node holds two "segments":
+ // one like the segments in the text anchor tree to support efficient offset changes
+ // and another that is the interval as seen by the user
+
+ // So basically, the tree contains a list of contiguous node segments of the first kind,
+ // with interval segments starting at the end of every node segment.
+
+ // Performance:
+ // Add is O(lg n)
+ // Remove is O(lg n)
+ // DocumentChanged is O(m * lg n), with m the number of segments that intersect with the changed document section
+ // FindFirstSegmentWithStartAfter is O(m + lg n) with m being the number of segments at the same offset as the result segment
+ // FindIntersectingSegments is O(m + lg n) with m being the number of intersecting segments.
+
+ int count;
+ TextSegment root;
+ bool isConnectedToDocument;
+
+ #region Constructor
+ /// <summary>
+ /// Creates a new TextSegmentCollection that needs manual calls to <see cref="UpdateOffsets(DocumentChangeEventArgs)"/>.
+ /// </summary>
+ public TextSegmentCollection()
+ {
+ }
+
+ /// <summary>
+ /// Creates a new TextSegmentCollection that updates the offsets automatically.
+ /// </summary>
+ /// <param name="textDocument">The document to which the text segments
+ /// that will be added to the tree belong. When the document changes, the
+ /// position of the text segments will be updated accordingly.</param>
+ public TextSegmentCollection(TextDocument textDocument)
+ {
+ if (textDocument == null)
+ throw new ArgumentNullException("textDocument");
+
+ textDocument.VerifyAccess();
+ isConnectedToDocument = true;
+ TextDocumentWeakEventManager.Changed.AddListener(textDocument, this);
+ }
+ #endregion
+
+ #region OnDocumentChanged / UpdateOffsets
+ bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
+ {
+ if (managerType == typeof(TextDocumentWeakEventManager.Changed)) {
+ OnDocumentChanged((DocumentChangeEventArgs)e);
+ return true;
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// Updates the start and end offsets of all segments stored in this collection.
+ /// </summary>
+ /// <param name="e">DocumentChangeEventArgs instance describing the change to the document.</param>
+ public void UpdateOffsets(DocumentChangeEventArgs e)
+ {
+ if (e == null)
+ throw new ArgumentNullException("e");
+ if (isConnectedToDocument)
+ throw new InvalidOperationException("This TextSegmentCollection will automatically update offsets; do not call UpdateOffsets manually!");
+ OnDocumentChanged(e);
+ CheckProperties();
+ }
+
+ void OnDocumentChanged(DocumentChangeEventArgs e)
+ {
+ OffsetChangeMap map = e.OffsetChangeMapOrNull;
+ if (map != null) {
+ foreach (OffsetChangeMapEntry entry in map) {
+ UpdateOffsetsInternal(entry);
+ }
+ } else {
+ UpdateOffsetsInternal(e.CreateSingleChangeMapEntry());
+ }
+ }
+
+ /// <summary>
+ /// Updates the start and end offsets of all segments stored in this collection.
+ /// </summary>
+ /// <param name="change">OffsetChangeMapEntry instance describing the change to the document.</param>
+ public void UpdateOffsets(OffsetChangeMapEntry change)
+ {
+ if (isConnectedToDocument)
+ throw new InvalidOperationException("This TextSegmentCollection will automatically update offsets; do not call UpdateOffsets manually!");
+ UpdateOffsetsInternal(change);
+ CheckProperties();
+ }
+ #endregion
+
+ #region UpdateOffsets (implementation)
+ void UpdateOffsetsInternal(OffsetChangeMapEntry change)
+ {
+ // Special case pure insertions, because they don't always cause a text segment to increase in size when the replaced region
+ // is inside a segment (when offset is at start or end of a text semgent).
+ if (change.RemovalLength == 0) {
+ InsertText(change.Offset, change.InsertionLength);
+ } else {
+ ReplaceText(change);
+ }
+ }
+
+ void InsertText(int offset, int length)
+ {
+ if (length == 0)
+ return;
+
+ // enlarge segments that contain offset (excluding those that have offset as endpoint)
+ foreach (TextSegment segment in FindSegmentsContaining(offset)) {
+ if (segment.StartOffset < offset && offset < segment.EndOffset) {
+ segment.Length += length;
+ }
+ }
+
+ // move start offsets of all segments >= offset
+ TextSegment node = FindFirstSegmentWithStartAfter(offset);
+ if (node != null) {
+ node.nodeLength += length;
+ UpdateAugmentedData(node);
+ }
+ }
+
+ void ReplaceText(OffsetChangeMapEntry change)
+ {
+ Debug.Assert(change.RemovalLength > 0);
+ int offset = change.Offset;
+ foreach (TextSegment segment in FindOverlappingSegments(offset, change.RemovalLength)) {
+ if (segment.StartOffset <= offset) {
+ if (segment.EndOffset >= offset + change.RemovalLength) {
+ // Replacement inside segment: adjust segment length
+ segment.Length += change.InsertionLength - change.RemovalLength;
+ } else {
+ // Replacement starting inside segment and ending after segment end: set segment end to removal position
+ //segment.EndOffset = offset;
+ segment.Length = offset - segment.StartOffset;
+ }
+ } else {
+ // Replacement starting in front of text segment and running into segment.
+ // Keep segment.EndOffset constant and move segment.StartOffset to the end of the replacement
+ int remainingLength = segment.EndOffset - (offset + change.RemovalLength);
+ RemoveSegment(segment);
+ segment.StartOffset = offset + change.RemovalLength;
+ segment.Length = Math.Max(0, remainingLength);
+ AddSegment(segment);
+ }
+ }
+ // move start offsets of all segments > offset
+ TextSegment node = FindFirstSegmentWithStartAfter(offset + 1);
+ if (node != null) {
+ Debug.Assert(node.nodeLength >= change.RemovalLength);
+ node.nodeLength += change.InsertionLength - change.RemovalLength;
+ UpdateAugmentedData(node);
+ }
+ }
+ #endregion
+
+ #region Add
+ /// <summary>
+ /// Adds the specified segment to the tree. This will cause the segment to update when the
+ /// document changes.
+ /// </summary>
+ public void Add(T item)
+ {
+ if (item == null)
+ throw new ArgumentNullException("item");
+ if (item.ownerTree != null)
+ throw new ArgumentException("The segment is already added to a SegmentCollection.");
+ AddSegment(item);
+ }
+
+ void ISegmentTree.Add(TextSegment s)
+ {
+ AddSegment(s);
+ }
+
+ void AddSegment(TextSegment node)
+ {
+ int insertionOffset = node.StartOffset;
+ node.distanceToMaxEnd = node.segmentLength;
+ if (root == null) {
+ root = node;
+ node.totalNodeLength = node.nodeLength;
+ } else if (insertionOffset >= root.totalNodeLength) {
+ // append segment at end of tree
+ node.nodeLength = node.totalNodeLength = insertionOffset - root.totalNodeLength;
+ InsertAsRight(root.RightMost, node);
+ } else {
+ // insert in middle of tree
+ TextSegment n = FindNode(ref insertionOffset);
+ Debug.Assert(insertionOffset < n.nodeLength);
+ // split node segment 'n' at offset
+ node.totalNodeLength = node.nodeLength = insertionOffset;
+ n.nodeLength -= insertionOffset;
+ InsertBefore(n, node);
+ }
+ node.ownerTree = this;
+ count++;
+ CheckProperties();
+ }
+
+ void InsertBefore(TextSegment node, TextSegment newNode)
+ {
+ if (node.left == null) {
+ InsertAsLeft(node, newNode);
+ } else {
+ InsertAsRight(node.left.RightMost, newNode);
+ }
+ }
+ #endregion
+
+ #region GetNextSegment / GetPreviousSegment
+ /// <summary>
+ /// Gets the next segment after the specified segment.
+ /// Segments are sorted by their start offset.
+ /// Returns null if segment is the last segment.
+ /// </summary>
+ public T GetNextSegment(T segment)
+ {
+ if (!Contains(segment))
+ throw new ArgumentException("segment is not inside the segment tree");
+ return (T)segment.Successor;
+ }
+
+ /// <summary>
+ /// Gets the previous segment before the specified segment.
+ /// Segments are sorted by their start offset.
+ /// Returns null if segment is the first segment.
+ /// </summary>
+ public T GetPreviousSegment(T segment)
+ {
+ if (!Contains(segment))
+ throw new ArgumentException("segment is not inside the segment tree");
+ return (T)segment.Predecessor;
+ }
+ #endregion
+
+ #region FirstSegment/LastSegment
+ /// <summary>
+ /// Returns the first segment in the collection or null, if the collection is empty.
+ /// </summary>
+ public T FirstSegment {
+ get {
+ return root == null ? null : (T)root.LeftMost;
+ }
+ }
+
+ /// <summary>
+ /// Returns the last segment in the collection or null, if the collection is empty.
+ /// </summary>
+ public T LastSegment {
+ get {
+ return root == null ? null : (T)root.RightMost;
+ }
+ }
+ #endregion
+
+ #region FindFirstSegmentWithStartAfter
+ /// <summary>
+ /// Gets the first segment with a start offset greater or equal to <paramref name="startOffset"/>.
+ /// Returns null if no such segment is found.
+ /// </summary>
+ public T FindFirstSegmentWithStartAfter(int startOffset)
+ {
+ if (root == null)
+ return null;
+ if (startOffset <= 0)
+ return (T)root.LeftMost;
+ TextSegment s = FindNode(ref startOffset);
+ // startOffset means that the previous segment is starting at the offset we were looking for
+ while (startOffset == 0) {
+ TextSegment p = (s == null) ? root.RightMost : s.Predecessor;
+ // There must always be a predecessor: if we were looking for the first node, we would have already
+ // returned it as root.LeftMost above.
+ Debug.Assert(p != null);
+ startOffset += p.nodeLength;
+ s = p;
+ }
+ return (T)s;
+ }
+
+ /// <summary>
+ /// Finds the node at the specified offset.
+ /// After the method has run, offset is relative to the beginning of the returned node.
+ /// </summary>
+ TextSegment FindNode(ref int offset)
+ {
+ TextSegment n = root;
+ while (true) {
+ if (n.left != null) {
+ if (offset < n.left.totalNodeLength) {
+ n = n.left; // descend into left subtree
+ continue;
+ } else {
+ offset -= n.left.totalNodeLength; // skip left subtree
+ }
+ }
+ if (offset < n.nodeLength) {
+ return n; // found correct node
+ } else {
+ offset -= n.nodeLength; // skip this node
+ }
+ if (n.right != null) {
+ n = n.right; // descend into right subtree
+ } else {
+ // didn't find any node containing the offset
+ return null;
+ }
+ }
+ }
+ #endregion
+
+ #region FindOverlappingSegments
+ /// <summary>
+ /// Finds all segments that contain the given offset.
+ /// (StartOffset &lt;= offset &lt;= EndOffset)
+ /// Segments are returned in the order given by GetNextSegment/GetPreviousSegment.
+ /// </summary>
+ /// <returns>Returns a new collection containing the results of the query.
+ /// This means it is safe to modify the TextSegmentCollection while iterating through the result collection.</returns>
+ public ReadOnlyCollection<T> FindSegmentsContaining(int offset)
+ {
+ return FindOverlappingSegments(offset, 0);
+ }
+
+ /// <summary>
+ /// Finds all segments that overlap with the given segment (including touching segments).
+ /// </summary>
+ /// <returns>Returns a new collection containing the results of the query.
+ /// This means it is safe to modify the TextSegmentCollection while iterating through the result collection.</returns>
+ public ReadOnlyCollection<T> FindOverlappingSegments(ISegment segment)
+ {
+ if (segment == null)
+ throw new ArgumentNullException("segment");
+ return FindOverlappingSegments(segment.Offset, segment.Length);
+ }
+
+ /// <summary>
+ /// Finds all segments that overlap with the given segment (including touching segments).
+ /// Segments are returned in the order given by GetNextSegment/GetPreviousSegment.
+ /// </summary>
+ /// <returns>Returns a new collection containing the results of the query.
+ /// This means it is safe to modify the TextSegmentCollection while iterating through the result collection.</returns>
+ public ReadOnlyCollection<T> FindOverlappingSegments(int offset, int length)
+ {
+ ThrowUtil.CheckNotNegative(length, "length");
+ List<T> results = new List<T>();
+ if (root != null) {
+ FindOverlappingSegments(results, root, offset, offset + length);
+ }
+ return results.AsReadOnly();
+ }
+
+ void FindOverlappingSegments(List<T> results, TextSegment node, int low, int high)
+ {
+ // low and high are relative to node.LeftMost startpos (not node.LeftMost.Offset)
+ if (high < 0) {
+ // node is irrelevant for search because all intervals in node are after high
+ return;
+ }
+
+ // find values relative to node.Offset
+ int nodeLow = low - node.nodeLength;
+ int nodeHigh = high - node.nodeLength;
+ if (node.left != null) {
+ nodeLow -= node.left.totalNodeLength;
+ nodeHigh -= node.left.totalNodeLength;
+ }
+
+ if (node.distanceToMaxEnd < nodeLow) {
+ // node is irrelevant for search because all intervals in node are before low
+ return;
+ }
+
+ if (node.left != null)
+ FindOverlappingSegments(results, node.left, low, high);
+
+ if (nodeHigh < 0) {
+ // node and everything in node.right is before low
+ return;
+ }
+
+ if (nodeLow <= node.segmentLength) {
+ results.Add((T)node);
+ }
+
+ if (node.right != null)
+ FindOverlappingSegments(results, node.right, nodeLow, nodeHigh);
+ }
+ #endregion
+
+ #region UpdateAugmentedData
+ void UpdateAugmentedData(TextSegment node)
+ {
+ int totalLength = node.nodeLength;
+ int distanceToMaxEnd = node.segmentLength;
+ if (node.left != null) {
+ totalLength += node.left.totalNodeLength;
+
+ int leftDTME = node.left.distanceToMaxEnd;
+ // dtme is relative, so convert it to the coordinates of node:
+ if (node.left.right != null)
+ leftDTME -= node.left.right.totalNodeLength;
+ leftDTME -= node.nodeLength;
+ if (leftDTME > distanceToMaxEnd)
+ distanceToMaxEnd = leftDTME;
+ }
+ if (node.right != null) {
+ totalLength += node.right.totalNodeLength;
+
+ int rightDTME = node.right.distanceToMaxEnd;
+ // dtme is relative, so convert it to the coordinates of node:
+ rightDTME += node.right.nodeLength;
+ if (node.right.left != null)
+ rightDTME += node.right.left.totalNodeLength;
+ if (rightDTME > distanceToMaxEnd)
+ distanceToMaxEnd = rightDTME;
+ }
+ if (node.totalNodeLength != totalLength
+ || node.distanceToMaxEnd != distanceToMaxEnd)
+ {
+ node.totalNodeLength = totalLength;
+ node.distanceToMaxEnd = distanceToMaxEnd;
+ if (node.parent != null)
+ UpdateAugmentedData(node.parent);
+ }
+ }
+
+ void ISegmentTree.UpdateAugmentedData(TextSegment node)
+ {
+ UpdateAugmentedData(node);
+ }
+ #endregion
+
+ #region Remove
+ /// <summary>
+ /// Removes the specified segment from the tree. This will cause the segment to not update
+ /// anymore when the document changes.
+ /// </summary>
+ public bool Remove(T item)
+ {
+ if (!Contains(item))
+ return false;
+ RemoveSegment(item);
+ return true;
+ }
+
+ void ISegmentTree.Remove(TextSegment s)
+ {
+ RemoveSegment(s);
+ }
+
+ void RemoveSegment(TextSegment s)
+ {
+ int oldOffset = s.StartOffset;
+ TextSegment successor = s.Successor;
+ if (successor != null)
+ successor.nodeLength += s.nodeLength;
+ RemoveNode(s);
+ if (successor != null)
+ UpdateAugmentedData(successor);
+ Disconnect(s, oldOffset);
+ CheckProperties();
+ }
+
+ void Disconnect(TextSegment s, int offset)
+ {
+ s.left = s.right = s.parent = null;
+ s.ownerTree = null;
+ s.nodeLength = offset;
+ count--;
+ }
+
+ /// <summary>
+ /// Removes all segments from the tree.
+ /// </summary>
+ public void Clear()
+ {
+ T[] segments = this.ToArray();
+ root = null;
+ int offset = 0;
+ foreach (TextSegment s in segments) {
+ offset += s.nodeLength;
+ Disconnect(s, offset);
+ }
+ CheckProperties();
+ }
+ #endregion
+
+ #region CheckProperties
+ [Conditional("DATACONSISTENCYTEST")]
+ internal void CheckProperties()
+ {
+ #if DEBUG
+ if (root != null) {
+ CheckProperties(root);
+
+ // check red-black property:
+ int blackCount = -1;
+ CheckNodeProperties(root, null, RED, 0, ref blackCount);
+ }
+
+ int expectedCount = 0;
+ // we cannot trust LINQ not to call ICollection.Count, so we need this loop
+ // to count the elements in the tree
+ using (IEnumerator<T> en = GetEnumerator()) {
+ while (en.MoveNext()) expectedCount++;
+ }
+ Debug.Assert(count == expectedCount);
+ #endif
+ }
+
+ #if DEBUG
+ void CheckProperties(TextSegment node)
+ {
+ int totalLength = node.nodeLength;
+ int distanceToMaxEnd = node.segmentLength;
+ if (node.left != null) {
+ CheckProperties(node.left);
+ totalLength += node.left.totalNodeLength;
+ distanceToMaxEnd = Math.Max(distanceToMaxEnd,
+ node.left.distanceToMaxEnd + node.left.StartOffset - node.StartOffset);
+ }
+ if (node.right != null) {
+ CheckProperties(node.right);
+ totalLength += node.right.totalNodeLength;
+ distanceToMaxEnd = Math.Max(distanceToMaxEnd,
+ node.right.distanceToMaxEnd + node.right.StartOffset - node.StartOffset);
+ }
+ Debug.Assert(node.totalNodeLength == totalLength);
+ Debug.Assert(node.distanceToMaxEnd == distanceToMaxEnd);
+ }
+
+ /*
+ 1. A node is either red or black.
+ 2. The root is black.
+ 3. All leaves are black. (The leaves are the NIL children.)
+ 4. Both children of every red node are black. (So every red node must have a black parent.)
+ 5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.)
+ */
+ void CheckNodeProperties(TextSegment node, TextSegment parentNode, bool parentColor, int blackCount, ref int expectedBlackCount)
+ {
+ if (node == null) return;
+
+ Debug.Assert(node.parent == parentNode);
+
+ if (parentColor == RED) {
+ Debug.Assert(node.color == BLACK);
+ }
+ if (node.color == BLACK) {
+ blackCount++;
+ }
+ if (node.left == null && node.right == null) {
+ // node is a leaf node:
+ if (expectedBlackCount == -1)
+ expectedBlackCount = blackCount;
+ else
+ Debug.Assert(expectedBlackCount == blackCount);
+ }
+ CheckNodeProperties(node.left, node, node.color, blackCount, ref expectedBlackCount);
+ CheckNodeProperties(node.right, node, node.color, blackCount, ref expectedBlackCount);
+ }
+
+ static void AppendTreeToString(TextSegment node, StringBuilder b, int indent)
+ {
+ if (node.color == RED)
+ b.Append("RED ");
+ else
+ b.Append("BLACK ");
+ b.AppendLine(node.ToString() + node.ToDebugString());
+ indent += 2;
+ if (node.left != null) {
+ b.Append(' ', indent);
+ b.Append("L: ");
+ AppendTreeToString(node.left, b, indent);
+ }
+ if (node.right != null) {
+ b.Append(' ', indent);
+ b.Append("R: ");
+ AppendTreeToString(node.right, b, indent);
+ }
+ }
+ #endif
+
+ internal string GetTreeAsString()
+ {
+ #if DEBUG
+ StringBuilder b = new StringBuilder();
+ if (root != null)
+ AppendTreeToString(root, b, 0);
+ return b.ToString();
+ #else
+ return "Not available in release build.";
+ #endif
+ }
+ #endregion
+
+ #region Red/Black Tree
+ internal const bool RED = true;
+ internal const bool BLACK = false;
+
+ void InsertAsLeft(TextSegment parentNode, TextSegment newNode)
+ {
+ Debug.Assert(parentNode.left == null);
+ parentNode.left = newNode;
+ newNode.parent = parentNode;
+ newNode.color = RED;
+ UpdateAugmentedData(parentNode);
+ FixTreeOnInsert(newNode);
+ }
+
+ void InsertAsRight(TextSegment parentNode, TextSegment newNode)
+ {
+ Debug.Assert(parentNode.right == null);
+ parentNode.right = newNode;
+ newNode.parent = parentNode;
+ newNode.color = RED;
+ UpdateAugmentedData(parentNode);
+ FixTreeOnInsert(newNode);
+ }
+
+ void FixTreeOnInsert(TextSegment node)
+ {
+ Debug.Assert(node != null);
+ Debug.Assert(node.color == RED);
+ Debug.Assert(node.left == null || node.left.color == BLACK);
+ Debug.Assert(node.right == null || node.right.color == BLACK);
+
+ TextSegment parentNode = node.parent;
+ if (parentNode == null) {
+ // we inserted in the root -> the node must be black
+ // since this is a root node, making the node black increments the number of black nodes
+ // on all paths by one, so it is still the same for all paths.
+ node.color = BLACK;
+ return;
+ }
+ if (parentNode.color == BLACK) {
+ // if the parent node where we inserted was black, our red node is placed correctly.
+ // since we inserted a red node, the number of black nodes on each path is unchanged
+ // -> the tree is still balanced
+ return;
+ }
+ // parentNode is red, so there is a conflict here!
+
+ // because the root is black, parentNode is not the root -> there is a grandparent node
+ TextSegment grandparentNode = parentNode.parent;
+ TextSegment uncleNode = Sibling(parentNode);
+ if (uncleNode != null && uncleNode.color == RED) {
+ parentNode.color = BLACK;
+ uncleNode.color = BLACK;
+ grandparentNode.color = RED;
+ FixTreeOnInsert(grandparentNode);
+ return;
+ }
+ // now we know: parent is red but uncle is black
+ // First rotation:
+ if (node == parentNode.right && parentNode == grandparentNode.left) {
+ RotateLeft(parentNode);
+ node = node.left;
+ } else if (node == parentNode.left && parentNode == grandparentNode.right) {
+ RotateRight(parentNode);
+ node = node.right;
+ }
+ // because node might have changed, reassign variables:
+ parentNode = node.parent;
+ grandparentNode = parentNode.parent;
+
+ // Now recolor a bit:
+ parentNode.color = BLACK;
+ grandparentNode.color = RED;
+ // Second rotation:
+ if (node == parentNode.left && parentNode == grandparentNode.left) {
+ RotateRight(grandparentNode);
+ } else {
+ // because of the first rotation, this is guaranteed:
+ Debug.Assert(node == parentNode.right && parentNode == grandparentNode.right);
+ RotateLeft(grandparentNode);
+ }
+ }
+
+ void RemoveNode(TextSegment removedNode)
+ {
+ if (removedNode.left != null && removedNode.right != null) {
+ // replace removedNode with it's in-order successor
+
+ TextSegment leftMost = removedNode.right.LeftMost;
+ RemoveNode(leftMost); // remove leftMost from its current location
+
+ // and overwrite the removedNode with it
+ ReplaceNode(removedNode, leftMost);
+ leftMost.left = removedNode.left;
+ if (leftMost.left != null) leftMost.left.parent = leftMost;
+ leftMost.right = removedNode.right;
+ if (leftMost.right != null) leftMost.right.parent = leftMost;
+ leftMost.color = removedNode.color;
+
+ UpdateAugmentedData(leftMost);
+ if (leftMost.parent != null) UpdateAugmentedData(leftMost.parent);
+ return;
+ }
+
+ // now either removedNode.left or removedNode.right is null
+ // get the remaining child
+ TextSegment parentNode = removedNode.parent;
+ TextSegment childNode = removedNode.left ?? removedNode.right;
+ ReplaceNode(removedNode, childNode);
+ if (parentNode != null) UpdateAugmentedData(parentNode);
+ if (removedNode.color == BLACK) {
+ if (childNode != null && childNode.color == RED) {
+ childNode.color = BLACK;
+ } else {
+ FixTreeOnDelete(childNode, parentNode);
+ }
+ }
+ }
+
+ void FixTreeOnDelete(TextSegment node, TextSegment parentNode)
+ {
+ Debug.Assert(node == null || node.parent == parentNode);
+ if (parentNode == null)
+ return;
+
+ // warning: node may be null
+ TextSegment sibling = Sibling(node, parentNode);
+ if (sibling.color == RED) {
+ parentNode.color = RED;
+ sibling.color = BLACK;
+ if (node == parentNode.left) {
+ RotateLeft(parentNode);
+ } else {
+ RotateRight(parentNode);
+ }
+
+ sibling = Sibling(node, parentNode); // update value of sibling after rotation
+ }
+
+ if (parentNode.color == BLACK
+ && sibling.color == BLACK
+ && GetColor(sibling.left) == BLACK
+ && GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ FixTreeOnDelete(parentNode, parentNode.parent);
+ return;
+ }
+
+ if (parentNode.color == RED
+ && sibling.color == BLACK
+ && GetColor(sibling.left) == BLACK
+ && GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ parentNode.color = BLACK;
+ return;
+ }
+
+ if (node == parentNode.left &&
+ sibling.color == BLACK &&
+ GetColor(sibling.left) == RED &&
+ GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ sibling.left.color = BLACK;
+ RotateRight(sibling);
+ }
+ else if (node == parentNode.right &&
+ sibling.color == BLACK &&
+ GetColor(sibling.right) == RED &&
+ GetColor(sibling.left) == BLACK)
+ {
+ sibling.color = RED;
+ sibling.right.color = BLACK;
+ RotateLeft(sibling);
+ }
+ sibling = Sibling(node, parentNode); // update value of sibling after rotation
+
+ sibling.color = parentNode.color;
+ parentNode.color = BLACK;
+ if (node == parentNode.left) {
+ if (sibling.right != null) {
+ Debug.Assert(sibling.right.color == RED);
+ sibling.right.color = BLACK;
+ }
+ RotateLeft(parentNode);
+ } else {
+ if (sibling.left != null) {
+ Debug.Assert(sibling.left.color == RED);
+ sibling.left.color = BLACK;
+ }
+ RotateRight(parentNode);
+ }
+ }
+
+ void ReplaceNode(TextSegment replacedNode, TextSegment newNode)
+ {
+ if (replacedNode.parent == null) {
+ Debug.Assert(replacedNode == root);
+ root = newNode;
+ } else {
+ if (replacedNode.parent.left == replacedNode)
+ replacedNode.parent.left = newNode;
+ else
+ replacedNode.parent.right = newNode;
+ }
+ if (newNode != null) {
+ newNode.parent = replacedNode.parent;
+ }
+ replacedNode.parent = null;
+ }
+
+ void RotateLeft(TextSegment p)
+ {
+ // let q be p's right child
+ TextSegment q = p.right;
+ Debug.Assert(q != null);
+ Debug.Assert(q.parent == p);
+ // set q to be the new root
+ ReplaceNode(p, q);
+
+ // set p's right child to be q's left child
+ p.right = q.left;
+ if (p.right != null) p.right.parent = p;
+ // set q's left child to be p
+ q.left = p;
+ p.parent = q;
+ UpdateAugmentedData(p);
+ UpdateAugmentedData(q);
+ }
+
+ void RotateRight(TextSegment p)
+ {
+ // let q be p's left child
+ TextSegment q = p.left;
+ Debug.Assert(q != null);
+ Debug.Assert(q.parent == p);
+ // set q to be the new root
+ ReplaceNode(p, q);
+
+ // set p's left child to be q's right child
+ p.left = q.right;
+ if (p.left != null) p.left.parent = p;
+ // set q's right child to be p
+ q.right = p;
+ p.parent = q;
+ UpdateAugmentedData(p);
+ UpdateAugmentedData(q);
+ }
+
+ static TextSegment Sibling(TextSegment node)
+ {
+ if (node == node.parent.left)
+ return node.parent.right;
+ else
+ return node.parent.left;
+ }
+
+ static TextSegment Sibling(TextSegment node, TextSegment parentNode)
+ {
+ Debug.Assert(node == null || node.parent == parentNode);
+ if (node == parentNode.left)
+ return parentNode.right;
+ else
+ return parentNode.left;
+ }
+
+ static bool GetColor(TextSegment node)
+ {
+ return node != null ? node.color : BLACK;
+ }
+ #endregion
+
+ #region ICollection<T> implementation
+ /// <summary>
+ /// Gets the number of segments in the tree.
+ /// </summary>
+ public int Count {
+ get { return count; }
+ }
+
+ bool ICollection<T>.IsReadOnly {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Gets whether this tree contains the specified item.
+ /// </summary>
+ public bool Contains(T item)
+ {
+ return item != null && item.ownerTree == this;
+ }
+
+ /// <summary>
+ /// Copies all segments in this SegmentTree to the specified array.
+ /// </summary>
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ if (array == null)
+ throw new ArgumentNullException("array");
+ if (array.Length < this.Count)
+ throw new ArgumentException("The array is too small", "array");
+ if (arrayIndex < 0 || arrayIndex + count > array.Length)
+ throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, "Value must be between 0 and " + (array.Length - count));
+ foreach (T s in this) {
+ array[arrayIndex++] = s;
+ }
+ }
+
+ /// <summary>
+ /// Gets an enumerator to enumerate the segments.
+ /// </summary>
+ public IEnumerator<T> GetEnumerator()
+ {
+ if (root != null) {
+ TextSegment current = root.LeftMost;
+ while (current != null) {
+ yield return (T)current;
+ // TODO: check if collection was modified during enumeration
+ current = current.Successor;
+ }
+ }
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+ #endregion
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextUtilities.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextUtilities.cs
new file mode 100644
index 000000000..a0428c4e3
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextUtilities.cs
@@ -0,0 +1,332 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Globalization;
+using System.Windows.Documents;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Specifies the mode for getting the next caret position.
+ /// </summary>
+ public enum CaretPositioningMode
+ {
+ /// <summary>
+ /// Normal positioning (stop at every caret position)
+ /// </summary>
+ Normal,
+ /// <summary>
+ /// Stop only on word borders.
+ /// </summary>
+ WordBorder,
+ /// <summary>
+ /// Stop only at the beginning of words. This is used for Ctrl+Left/Ctrl+Right.
+ /// </summary>
+ WordStart,
+ /// <summary>
+ /// Stop only at the beginning of words, and anywhere in the middle of symbols.
+ /// </summary>
+ WordStartOrSymbol,
+ /// <summary>
+ /// Stop only on word borders, and anywhere in the middle of symbols.
+ /// </summary>
+ WordBorderOrSymbol
+ }
+
+ /// <summary>
+ /// Static helper methods for working with text.
+ /// </summary>
+ public static partial class TextUtilities
+ {
+ #region GetControlCharacterName
+ // the names of the first 32 ASCII characters = Unicode C0 block
+ static readonly string[] c0Table = {
+ "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", "BS", "HT",
+ "LF", "VT", "FF", "CR", "SO", "SI", "DLE", "DC1", "DC2", "DC3",
+ "DC4", "NAK", "SYN", "ETB", "CAN", "EM", "SUB", "ESC", "FS", "GS",
+ "RS", "US"
+ };
+
+ // DEL (ASCII 127) and
+ // the names of the control characters in the C1 block (Unicode 128 to 159)
+ static readonly string[] delAndC1Table = {
+ "DEL",
+ "PAD", "HOP", "BPH", "NBH", "IND", "NEL", "SSA", "ESA", "HTS", "HTJ",
+ "VTS", "PLD", "PLU", "RI", "SS2", "SS3", "DCS", "PU1", "PU2", "STS",
+ "CCH", "MW", "SPA", "EPA", "SOS", "SGCI", "SCI", "CSI", "ST", "OSC",
+ "PM", "APC"
+ };
+
+ /// <summary>
+ /// Gets the name of the control character.
+ /// For unknown characters, the unicode codepoint is returned as 4-digit hexadecimal value.
+ /// </summary>
+ public static string GetControlCharacterName(char controlCharacter)
+ {
+ int num = (int)controlCharacter;
+ if (num < c0Table.Length)
+ return c0Table[num];
+ else if (num >= 127 && num <= 159)
+ return delAndC1Table[num - 127];
+ else
+ return num.ToString("x4", CultureInfo.InvariantCulture);
+ }
+ #endregion
+
+ #region GetWhitespace
+ /// <summary>
+ /// Gets all whitespace (' ' and '\t', but no newlines) after offset.
+ /// </summary>
+ /// <param name="textSource">The text source.</param>
+ /// <param name="offset">The offset where the whitespace starts.</param>
+ /// <returns>The segment containing the whitespace.</returns>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
+ Justification = "WPF uses 'Whitespace'")]
+ public static ISegment GetWhitespaceAfter(ITextSource textSource, int offset)
+ {
+ if (textSource == null)
+ throw new ArgumentNullException("textSource");
+ int pos;
+ for (pos = offset; pos < textSource.TextLength; pos++) {
+ char c = textSource.GetCharAt(pos);
+ if (c != ' ' && c != '\t')
+ break;
+ }
+ return new SimpleSegment(offset, pos - offset);
+ }
+
+ /// <summary>
+ /// Gets all whitespace (' ' and '\t', but no newlines) before offset.
+ /// </summary>
+ /// <param name="textSource">The text source.</param>
+ /// <param name="offset">The offset where the whitespace ends.</param>
+ /// <returns>The segment containing the whitespace.</returns>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
+ Justification = "WPF uses 'Whitespace'")]
+ public static ISegment GetWhitespaceBefore(ITextSource textSource, int offset)
+ {
+ if (textSource == null)
+ throw new ArgumentNullException("textSource");
+ int pos;
+ for (pos = offset - 1; pos >= 0; pos--) {
+ char c = textSource.GetCharAt(pos);
+ if (c != ' ' && c != '\t')
+ break;
+ }
+ pos++; // go back the one character that isn't whitespace
+ return new SimpleSegment(pos, offset - pos);
+ }
+
+ /// <summary>
+ /// Gets the leading whitespace segment on the document line.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
+ Justification = "WPF uses 'Whitespace'")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
+ Justification = "Parameter cannot be ITextSource because it must belong to the DocumentLine")]
+ public static ISegment GetLeadingWhitespace(TextDocument document, DocumentLine documentLine)
+ {
+ if (documentLine == null)
+ throw new ArgumentNullException("documentLine");
+ return GetWhitespaceAfter(document, documentLine.Offset);
+ }
+
+ /// <summary>
+ /// Gets the trailing whitespace segment on the document line.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
+ Justification = "WPF uses 'Whitespace'")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
+ Justification = "Parameter cannot be ITextSource because it must belong to the DocumentLine")]
+ public static ISegment GetTrailingWhitespace(TextDocument document, DocumentLine documentLine)
+ {
+ if (documentLine == null)
+ throw new ArgumentNullException("documentLine");
+ ISegment segment = GetWhitespaceBefore(document, documentLine.EndOffset);
+ // If the whole line consists of whitespace, we consider all of it as leading whitespace,
+ // so return an empty segment as trailing whitespace.
+ if (segment.Offset == documentLine.Offset)
+ return new SimpleSegment(documentLine.EndOffset, 0);
+ else
+ return segment;
+ }
+ #endregion
+
+ #region GetSingleIndentationSegment
+ /// <summary>
+ /// Gets a single indentation segment starting at <paramref name="offset"/> - at most one tab
+ /// or <paramref name="indentationSize"/> spaces.
+ /// </summary>
+ /// <param name="textSource">The text source.</param>
+ /// <param name="offset">The offset where the indentation segment starts.</param>
+ /// <param name="indentationSize">The size of an indentation unit. See <see cref="TextEditorOptions.IndentationSize"/>.</param>
+ /// <returns>The indentation segment.
+ /// If there is no indentation character at the specified <paramref name="offset"/>,
+ /// an empty segment is returned.</returns>
+ public static ISegment GetSingleIndentationSegment(ITextSource textSource, int offset, int indentationSize)
+ {
+ if (textSource == null)
+ throw new ArgumentNullException("textSource");
+ int pos = offset;
+ while (pos < textSource.TextLength) {
+ char c = textSource.GetCharAt(pos);
+ if (c == '\t') {
+ if (pos == offset)
+ return new SimpleSegment(offset, 1);
+ else
+ break;
+ } else if (c == ' ') {
+ if (pos - offset >= indentationSize)
+ break;
+ } else {
+ break;
+ }
+ // continue only if c==' ' and (pos-offset)<tabSize
+ pos++;
+ }
+ return new SimpleSegment(offset, pos - offset);
+ }
+ #endregion
+
+ #region GetCharacterClass
+ /// <summary>
+ /// Gets whether the character is whitespace, part of an identifier, or line terminator.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "c")]
+ public static CharacterClass GetCharacterClass(char c)
+ {
+ if (c == '\r' || c == '\n')
+ return CharacterClass.LineTerminator;
+ else if (char.IsWhiteSpace(c))
+ return CharacterClass.Whitespace;
+ else if (char.IsLetterOrDigit(c) || c == '_')
+ return CharacterClass.IdentifierPart;
+ else
+ return CharacterClass.Other;
+ }
+ #endregion
+
+ #region GetNextCaretPosition
+ /// <summary>
+ /// Gets the next caret position.
+ /// </summary>
+ /// <param name="textSource">The text source.</param>
+ /// <param name="offset">The start offset inside the text source.</param>
+ /// <param name="direction">The search direction (forwards or backwards).</param>
+ /// <param name="mode">The mode for caret positioning.</param>
+ /// <returns>The offset of the next caret position, or -1 if there is no further caret position
+ /// in the text source.</returns>
+ /// <remarks>
+ /// This method is NOT equivalent to the actual caret movement when using VisualLine.GetNextCaretPosition.
+ /// In real caret movement, there are additional caret stops at line starts and ends. This method
+ /// treats linefeeds as simple whitespace.
+ /// </remarks>
+ public static int GetNextCaretPosition(ITextSource textSource, int offset, LogicalDirection direction, CaretPositioningMode mode)
+ {
+ if (textSource == null)
+ throw new ArgumentNullException("textSource");
+ if (mode != CaretPositioningMode.Normal
+ && mode != CaretPositioningMode.WordBorder
+ && mode != CaretPositioningMode.WordStart
+ && mode != CaretPositioningMode.WordBorderOrSymbol
+ && mode != CaretPositioningMode.WordStartOrSymbol)
+ {
+ throw new ArgumentException("Unsupported CaretPositioningMode: " + mode, "mode");
+ }
+ if (direction != LogicalDirection.Backward
+ && direction != LogicalDirection.Forward)
+ {
+ throw new ArgumentException("Invalid LogicalDirection: " + direction, "direction");
+ }
+ int textLength = textSource.TextLength;
+ if (textLength <= 0) {
+ // empty document? has a normal caret position at 0, though no word borders
+ if (mode == CaretPositioningMode.Normal) {
+ if (offset > 0 && direction == LogicalDirection.Backward) return 0;
+ if (offset < 0 && direction == LogicalDirection.Forward) return 0;
+ }
+ return -1;
+ }
+ while (true) {
+ int nextPos = (direction == LogicalDirection.Backward) ? offset - 1 : offset + 1;
+
+ // return -1 if there is no further caret position in the text source
+ // we also need this to handle offset values outside the valid range
+ if (nextPos < 0 || nextPos > textLength)
+ return -1;
+
+ // stop at every caret position? we can stop immediately.
+ if (mode == CaretPositioningMode.Normal)
+ return nextPos;
+ // not normal mode? we're looking for word borders...
+
+ // check if we've run against the textSource borders.
+ // a 'textSource' usually isn't the whole document, but a single VisualLineElement.
+ if (nextPos == 0) {
+ // at the document start, there's only a word border
+ // if the first character is not whitespace
+ if (!char.IsWhiteSpace(textSource.GetCharAt(0)))
+ return nextPos;
+ } else if (nextPos == textLength) {
+ // at the document end, there's never a word start
+ if (mode != CaretPositioningMode.WordStart && mode != CaretPositioningMode.WordStartOrSymbol) {
+ // at the document end, there's only a word border
+ // if the last character is not whitespace
+ if (!char.IsWhiteSpace(textSource.GetCharAt(textLength - 1)))
+ return nextPos;
+ }
+ } else {
+ CharacterClass charBefore = GetCharacterClass(textSource.GetCharAt(nextPos - 1));
+ CharacterClass charAfter = GetCharacterClass(textSource.GetCharAt(nextPos));
+ if (charBefore == charAfter) {
+ if (charBefore == CharacterClass.Other &&
+ (mode == CaretPositioningMode.WordBorderOrSymbol || mode == CaretPositioningMode.WordStartOrSymbol))
+ {
+ // With the "OrSymbol" modes, there's a word border and start between any two unknown characters
+ return nextPos;
+ }
+ } else {
+ // this looks like a possible border
+
+ // if we're looking for word starts, check that this is a word start (and not a word end)
+ // if we're just checking for word borders, accept unconditionally
+ if (!((mode == CaretPositioningMode.WordStart || mode == CaretPositioningMode.WordStartOrSymbol)
+ && (charAfter == CharacterClass.Whitespace || charAfter == CharacterClass.LineTerminator)))
+ {
+ return nextPos;
+ }
+ }
+ }
+ // we'll have to continue searching...
+ offset = nextPos;
+ }
+ }
+ #endregion
+ }
+
+ /// <summary>
+ /// Classifies a character as whitespace, line terminator, part of an identifier, or other.
+ /// </summary>
+ public enum CharacterClass
+ {
+ /// <summary>
+ /// The character is not whitespace, line terminator or part of an identifier.
+ /// </summary>
+ Other,
+ /// <summary>
+ /// The character is whitespace (but not line terminator).
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
+ Justification = "WPF uses 'Whitespace'")]
+ Whitespace,
+ /// <summary>
+ /// The character can be part of an identifier (Letter, digit or underscore).
+ /// </summary>
+ IdentifierPart,
+ /// <summary>
+ /// The character is line terminator (\r or \n).
+ /// </summary>
+ LineTerminator
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoOperationGroup.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoOperationGroup.cs
new file mode 100644
index 000000000..12afe3838
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoOperationGroup.cs
@@ -0,0 +1,61 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Diagnostics;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// This class stacks the last x operations from the undostack and makes
+ /// one undo/redo operation from it.
+ /// </summary>
+ sealed class UndoOperationGroup : IUndoableOperationWithContext
+ {
+ IUndoableOperation[] undolist;
+
+ public UndoOperationGroup(Deque<IUndoableOperation> stack, int numops)
+ {
+ if (stack == null) {
+ throw new ArgumentNullException("stack");
+ }
+
+ Debug.Assert(numops > 0 , "UndoOperationGroup : numops should be > 0");
+ Debug.Assert(numops <= stack.Count);
+
+ undolist = new IUndoableOperation[numops];
+ for (int i = 0; i < numops; ++i) {
+ undolist[i] = stack.PopBack();
+ }
+ }
+
+ public void Undo()
+ {
+ for (int i = 0; i < undolist.Length; ++i) {
+ undolist[i].Undo();
+ }
+ }
+
+ public void Undo(UndoStack stack)
+ {
+ for (int i = 0; i < undolist.Length; ++i) {
+ stack.RunUndo(undolist[i]);
+ }
+ }
+
+ public void Redo()
+ {
+ for (int i = undolist.Length - 1; i >= 0; --i) {
+ undolist[i].Redo();
+ }
+ }
+
+ public void Redo(UndoStack stack)
+ {
+ for (int i = undolist.Length - 1; i >= 0; --i) {
+ stack.RunRedo(undolist[i]);
+ }
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoStack.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoStack.cs
new file mode 100644
index 000000000..f0a759b23
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoStack.cs
@@ -0,0 +1,450 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Undo stack implementation.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
+ public sealed class UndoStack : INotifyPropertyChanged
+ {
+ /// undo stack is listening for changes
+ internal const int StateListen = 0;
+ /// undo stack is reverting/repeating a set of changes
+ internal const int StatePlayback = 1;
+ // undo stack is reverting/repeating a set of changes and modifies the document to do this
+ internal const int StatePlaybackModifyDocument = 2;
+ /// state is used for checking that noone but the UndoStack performs changes
+ /// during Undo events
+ internal int state = StateListen;
+
+ Deque<IUndoableOperation> undostack = new Deque<IUndoableOperation>();
+ Deque<IUndoableOperation> redostack = new Deque<IUndoableOperation>();
+ int sizeLimit = int.MaxValue;
+
+ int undoGroupDepth;
+ int actionCountInUndoGroup;
+ int optionalActionCount;
+ object lastGroupDescriptor;
+ bool allowContinue;
+
+ #region IsOriginalFile implementation
+ // implements feature request SD2-784 - File still considered dirty after undoing all changes
+
+ /// <summary>
+ /// Number of times undo must be executed until the original state is reached.
+ /// Negative: number of times redo must be executed until the original state is reached.
+ /// Special case: int.MinValue == original state is unreachable
+ /// </summary>
+ int elementsOnUndoUntilOriginalFile;
+
+ bool isOriginalFile = true;
+
+ /// <summary>
+ /// Gets whether the document is currently in its original state (no modifications).
+ /// </summary>
+ public bool IsOriginalFile {
+ get { return isOriginalFile; }
+ }
+
+ void RecalcIsOriginalFile()
+ {
+ bool newIsOriginalFile = (elementsOnUndoUntilOriginalFile == 0);
+ if (newIsOriginalFile != isOriginalFile) {
+ isOriginalFile = newIsOriginalFile;
+ NotifyPropertyChanged("IsOriginalFile");
+ }
+ }
+
+ /// <summary>
+ /// Marks the current state as original. Discards any previous "original" markers.
+ /// </summary>
+ public void MarkAsOriginalFile()
+ {
+ elementsOnUndoUntilOriginalFile = 0;
+ RecalcIsOriginalFile();
+ }
+
+ /// <summary>
+ /// Discards the current "original" marker.
+ /// </summary>
+ public void DiscardOriginalFileMarker()
+ {
+ elementsOnUndoUntilOriginalFile = int.MinValue;
+ RecalcIsOriginalFile();
+ }
+
+ void FileModified(int newElementsOnUndoStack)
+ {
+ if (elementsOnUndoUntilOriginalFile == int.MinValue)
+ return;
+
+ elementsOnUndoUntilOriginalFile += newElementsOnUndoStack;
+ if (elementsOnUndoUntilOriginalFile > undostack.Count)
+ elementsOnUndoUntilOriginalFile = int.MinValue;
+
+ // don't call RecalcIsOriginalFile(): wait until end of undo group
+ }
+ #endregion
+
+ /// <summary>
+ /// Gets if the undo stack currently accepts changes.
+ /// Is false while an undo action is running.
+ /// </summary>
+ public bool AcceptChanges {
+ get { return state == StateListen; }
+ }
+
+ /// <summary>
+ /// Gets if there are actions on the undo stack.
+ /// Use the PropertyChanged event to listen to changes of this property.
+ /// </summary>
+ public bool CanUndo {
+ get { return undostack.Count > 0; }
+ }
+
+ /// <summary>
+ /// Gets if there are actions on the redo stack.
+ /// Use the PropertyChanged event to listen to changes of this property.
+ /// </summary>
+ public bool CanRedo {
+ get { return redostack.Count > 0; }
+ }
+
+ /// <summary>
+ /// Gets/Sets the limit on the number of items on the undo stack.
+ /// </summary>
+ /// <remarks>The size limit is enforced only on the number of stored top-level undo groups.
+ /// Elements within undo groups do not count towards the size limit.</remarks>
+ public int SizeLimit {
+ get { return sizeLimit; }
+ set {
+ if (value < 0)
+ ThrowUtil.CheckNotNegative(value, "value");
+ if (sizeLimit != value) {
+ sizeLimit = value;
+ NotifyPropertyChanged("SizeLimit");
+ if (undoGroupDepth == 0)
+ EnforceSizeLimit();
+ }
+ }
+ }
+
+ void EnforceSizeLimit()
+ {
+ Debug.Assert(undoGroupDepth == 0);
+ while (undostack.Count > sizeLimit)
+ undostack.PopFront();
+ while (redostack.Count > sizeLimit)
+ redostack.PopFront();
+ }
+
+ /// <summary>
+ /// If an undo group is open, gets the group descriptor of the current top-level
+ /// undo group.
+ /// If no undo group is open, gets the group descriptor from the previous undo group.
+ /// </summary>
+ /// <remarks>The group descriptor can be used to join adjacent undo groups:
+ /// use a group descriptor to mark your changes, and on the second action,
+ /// compare LastGroupDescriptor and use <see cref="StartContinuedUndoGroup"/> if you
+ /// want to join the undo groups.</remarks>
+ public object LastGroupDescriptor {
+ get { return lastGroupDescriptor; }
+ }
+
+ /// <summary>
+ /// Starts grouping changes.
+ /// Maintains a counter so that nested calls are possible.
+ /// </summary>
+ public void StartUndoGroup()
+ {
+ StartUndoGroup(null);
+ }
+
+ /// <summary>
+ /// Starts grouping changes.
+ /// Maintains a counter so that nested calls are possible.
+ /// </summary>
+ /// <param name="groupDescriptor">An object that is stored with the undo group.
+ /// If this is not a top-level undo group, the parameter is ignored.</param>
+ public void StartUndoGroup(object groupDescriptor)
+ {
+ if (undoGroupDepth == 0) {
+ actionCountInUndoGroup = 0;
+ optionalActionCount = 0;
+ lastGroupDescriptor = groupDescriptor;
+ }
+ undoGroupDepth++;
+ //Util.LoggingService.Debug("Open undo group (new depth=" + undoGroupDepth + ")");
+ }
+
+ /// <summary>
+ /// Starts grouping changes, continuing with the previously closed undo group if possible.
+ /// Maintains a counter so that nested calls are possible.
+ /// If the call to StartContinuedUndoGroup is a nested call, it behaves exactly
+ /// as <see cref="StartUndoGroup()"/>, only top-level calls can continue existing undo groups.
+ /// </summary>
+ /// <param name="groupDescriptor">An object that is stored with the undo group.
+ /// If this is not a top-level undo group, the parameter is ignored.</param>
+ public void StartContinuedUndoGroup(object groupDescriptor)
+ {
+ if (undoGroupDepth == 0) {
+ actionCountInUndoGroup = (allowContinue && undostack.Count > 0) ? 1 : 0;
+ optionalActionCount = 0;
+ lastGroupDescriptor = groupDescriptor;
+ }
+ undoGroupDepth++;
+ //Util.LoggingService.Debug("Continue undo group (new depth=" + undoGroupDepth + ")");
+ }
+
+ /// <summary>
+ /// Stops grouping changes.
+ /// </summary>
+ public void EndUndoGroup()
+ {
+ if (undoGroupDepth == 0) return;
+ undoGroupDepth--;
+ //Util.LoggingService.Debug("Close undo group (new depth=" + undoGroupDepth + ")");
+ if (undoGroupDepth == 0) {
+ Debug.Assert(state == StateListen || actionCountInUndoGroup == 0);
+ if (actionCountInUndoGroup == optionalActionCount) {
+ // only optional actions: don't store them
+ for (int i = 0; i < optionalActionCount; i++) {
+ undostack.PopBack();
+ }
+ } else if (actionCountInUndoGroup > 1) {
+ // combine all actions within the group into a single grouped action
+ undostack.PushBack(new UndoOperationGroup(undostack, actionCountInUndoGroup));
+ FileModified(-actionCountInUndoGroup + 1 + optionalActionCount);
+ }
+ //if (state == StateListen) {
+ EnforceSizeLimit();
+ allowContinue = true;
+ RecalcIsOriginalFile(); // can raise event
+ //}
+ }
+ }
+
+ /// <summary>
+ /// Throws an InvalidOperationException if an undo group is current open.
+ /// </summary>
+ void ThrowIfUndoGroupOpen()
+ {
+ try
+ {
+ if (undoGroupDepth != 0)
+ {
+ undoGroupDepth = 0;
+ throw new InvalidOperationException("No undo group should be open at this point");
+ }
+ if (state != StateListen)
+ {
+ throw new InvalidOperationException("This method cannot be called while an undo operation is being performed");
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ List<TextDocument> affectedDocuments;
+
+ internal void RegisterAffectedDocument(TextDocument document)
+ {
+ if (affectedDocuments == null)
+ affectedDocuments = new List<TextDocument>();
+ if (!affectedDocuments.Contains(document)) {
+ affectedDocuments.Add(document);
+ document.BeginUpdate();
+ }
+ }
+
+ void CallEndUpdateOnAffectedDocuments()
+ {
+ if (affectedDocuments != null) {
+ foreach (TextDocument doc in affectedDocuments) {
+ doc.EndUpdate();
+ }
+ affectedDocuments = null;
+ }
+ }
+
+ /// <summary>
+ /// Call this method to undo the last operation on the stack
+ /// </summary>
+ public void Undo()
+ {
+ ThrowIfUndoGroupOpen();
+ if (undostack.Count > 0) {
+ // disallow continuing undo groups after undo operation
+ lastGroupDescriptor = null; allowContinue = false;
+ // fetch operation to undo and move it to redo stack
+ IUndoableOperation uedit = undostack.PopBack();
+ redostack.PushBack(uedit);
+ state = StatePlayback;
+ try {
+ RunUndo(uedit);
+ } finally {
+ state = StateListen;
+ FileModified(-1);
+ CallEndUpdateOnAffectedDocuments();
+ }
+ RecalcIsOriginalFile();
+ if (undostack.Count == 0)
+ NotifyPropertyChanged("CanUndo");
+ if (redostack.Count == 1)
+ NotifyPropertyChanged("CanRedo");
+ }
+ }
+
+ internal void RunUndo(IUndoableOperation op)
+ {
+ IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext;
+ if (opWithCtx != null)
+ opWithCtx.Undo(this);
+ else
+ op.Undo();
+ }
+
+ /// <summary>
+ /// Call this method to redo the last undone operation
+ /// </summary>
+ public void Redo()
+ {
+ ThrowIfUndoGroupOpen();
+ if (redostack.Count > 0) {
+ lastGroupDescriptor = null; allowContinue = false;
+ IUndoableOperation uedit = redostack.PopBack();
+ undostack.PushBack(uedit);
+ state = StatePlayback;
+ try {
+ RunRedo(uedit);
+ } finally {
+ state = StateListen;
+ FileModified(1);
+ CallEndUpdateOnAffectedDocuments();
+ }
+ RecalcIsOriginalFile();
+ if (redostack.Count == 0)
+ NotifyPropertyChanged("CanRedo");
+ if (undostack.Count == 1)
+ NotifyPropertyChanged("CanUndo");
+ }
+ }
+
+ internal void RunRedo(IUndoableOperation op)
+ {
+ IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext;
+ if (opWithCtx != null)
+ opWithCtx.Redo(this);
+ else
+ op.Redo();
+ }
+
+ /// <summary>
+ /// Call this method to push an UndoableOperation on the undostack.
+ /// The redostack will be cleared if you use this method.
+ /// </summary>
+ public void Push(IUndoableOperation operation)
+ {
+ Push(operation, false);
+ }
+
+ /// <summary>
+ /// Call this method to push an UndoableOperation on the undostack.
+ /// However, the operation will be only stored if the undo group contains a
+ /// non-optional operation.
+ /// Use this method to store the caret position/selection on the undo stack to
+ /// prevent having only actions that affect only the caret and not the document.
+ /// </summary>
+ public void PushOptional(IUndoableOperation operation)
+ {
+ if (undoGroupDepth == 0)
+ throw new InvalidOperationException("Cannot use PushOptional outside of undo group");
+ Push(operation, true);
+ }
+
+ void Push(IUndoableOperation operation, bool isOptional)
+ {
+ if (operation == null) {
+ throw new ArgumentNullException("operation");
+ }
+
+ if (state == StateListen && sizeLimit > 0) {
+ bool wasEmpty = undostack.Count == 0;
+
+ bool needsUndoGroup = undoGroupDepth == 0;
+ if (needsUndoGroup) StartUndoGroup();
+ undostack.PushBack(operation);
+ actionCountInUndoGroup++;
+ if (isOptional)
+ optionalActionCount++;
+ else
+ FileModified(1);
+ if (needsUndoGroup) EndUndoGroup();
+ if (wasEmpty)
+ NotifyPropertyChanged("CanUndo");
+ ClearRedoStack();
+ }
+ }
+
+ /// <summary>
+ /// Call this method, if you want to clear the redo stack
+ /// </summary>
+ public void ClearRedoStack()
+ {
+ if (redostack.Count != 0) {
+ redostack.Clear();
+ NotifyPropertyChanged("CanRedo");
+ // if the "original file" marker is on the redo stack: remove it
+ if (elementsOnUndoUntilOriginalFile < 0)
+ elementsOnUndoUntilOriginalFile = int.MinValue;
+ }
+ }
+
+ /// <summary>
+ /// Clears both the undo and redo stack.
+ /// </summary>
+ public void ClearAll()
+ {
+ ThrowIfUndoGroupOpen();
+ actionCountInUndoGroup = 0;
+ optionalActionCount = 0;
+ if (undostack.Count != 0) {
+ lastGroupDescriptor = null;
+ allowContinue = false;
+ undostack.Clear();
+ NotifyPropertyChanged("CanUndo");
+ }
+ ClearRedoStack();
+ }
+
+ internal void Push(TextDocument document, DocumentChangeEventArgs e)
+ {
+ if (state == StatePlayback)
+ throw new InvalidOperationException("Document changes during undo/redo operations are not allowed.");
+ if (state == StatePlaybackModifyDocument)
+ state = StatePlayback; // allow only 1 change per expected modification
+ else
+ Push(new DocumentChangeOperation(document, e));
+ }
+
+ /// <summary>
+ /// Is raised when a property (CanUndo, CanRedo) changed.
+ /// </summary>
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ void NotifyPropertyChanged(string propertyName)
+ {
+ if (PropertyChanged != null)
+ PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/WeakLineTracker.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/WeakLineTracker.cs
new file mode 100644
index 000000000..a70874645
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/WeakLineTracker.cs
@@ -0,0 +1,85 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Allows registering a line tracker on a TextDocument using a weak reference from the document to the line tracker.
+ /// </summary>
+ public sealed class WeakLineTracker : ILineTracker
+ {
+ TextDocument textDocument;
+ WeakReference targetObject;
+
+ private WeakLineTracker(TextDocument textDocument, ILineTracker targetTracker)
+ {
+ this.textDocument = textDocument;
+ this.targetObject = new WeakReference(targetTracker);
+ }
+
+ /// <summary>
+ /// Registers the <paramref name="targetTracker"/> as line tracker for the <paramref name="textDocument"/>.
+ /// A weak reference to the target tracker will be used, and the WeakLineTracker will deregister itself
+ /// when the target tracker is garbage collected.
+ /// </summary>
+ public static WeakLineTracker Register(TextDocument textDocument, ILineTracker targetTracker)
+ {
+ if (textDocument == null)
+ throw new ArgumentNullException("textDocument");
+ if (targetTracker == null)
+ throw new ArgumentNullException("targetTracker");
+ WeakLineTracker wlt = new WeakLineTracker(textDocument, targetTracker);
+ textDocument.LineTrackers.Add(wlt);
+ return wlt;
+ }
+
+ /// <summary>
+ /// Deregisters the weak line tracker.
+ /// </summary>
+ public void Deregister()
+ {
+ if (textDocument != null) {
+ textDocument.LineTrackers.Remove(this);
+ textDocument = null;
+ }
+ }
+
+ void ILineTracker.BeforeRemoveLine(DocumentLine line)
+ {
+ ILineTracker targetTracker = targetObject.Target as ILineTracker;
+ if (targetTracker != null)
+ targetTracker.BeforeRemoveLine(line);
+ else
+ Deregister();
+ }
+
+ void ILineTracker.SetLineLength(DocumentLine line, int newTotalLength)
+ {
+ ILineTracker targetTracker = targetObject.Target as ILineTracker;
+ if (targetTracker != null)
+ targetTracker.SetLineLength(line, newTotalLength);
+ else
+ Deregister();
+ }
+
+ void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine)
+ {
+ ILineTracker targetTracker = targetObject.Target as ILineTracker;
+ if (targetTracker != null)
+ targetTracker.LineInserted(insertionPos, newLine);
+ else
+ Deregister();
+ }
+
+ void ILineTracker.RebuildDocument()
+ {
+ ILineTracker targetTracker = targetObject.Target as ILineTracker;
+ if (targetTracker != null)
+ targetTracker.RebuildDocument();
+ else
+ Deregister();
+ }
+ }
+}