diff options
| author | Roy Ben Shabat <Roy.mail.net@gmail.com> | 2019-04-09 01:47:48 +0300 |
|---|---|---|
| committer | Roy Ben Shabat <Roy.mail.net@gmail.com> | 2019-04-09 01:47:48 +0300 |
| commit | 080f1697e97e13461ec6df4d31c8924d01257a1b (patch) | |
| tree | b1fe0285de7bc9bc52e9e2195e66fe022bf8f5b3 /Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document | |
| parent | 1608e69a417bc5e40a607c3958c4a60f19f66f1a (diff) | |
| download | Tango-080f1697e97e13461ec6df4d31c8924d01257a1b.tar.gz Tango-080f1697e97e13461ec6df4d31c8924d01257a1b.zip | |
MERGE
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document')
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 <= offset <= 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(); + } + } +} |
