From 080f1697e97e13461ec6df4d31c8924d01257a1b Mon Sep 17 00:00:00 2001 From: Roy Ben Shabat Date: Tue, 9 Apr 2019 01:47:48 +0300 Subject: MERGE --- .../Document/TextDocument.cs | 836 +++++++++++++++++++++ 1 file changed, 836 insertions(+) create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs') 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 +{ + /// + /// This class is the main class of the text model. Basically, it is a with events. + /// + /// + /// Thread safety: + /// + /// However, there is a single method that is thread-safe: (and its overloads). + /// + public sealed class TextDocument : ITextSource, INotifyPropertyChanged + { + #region Thread ownership + readonly object lockObject = new object(); + Thread owner = Thread.CurrentThread; + + /// + /// Verifies that the current thread is the documents owner thread. + /// Throws an if the wrong thread accesses the TextDocument. + /// + /// + /// The TextDocument class is not thread-safe. A document instance expects to have a single owner thread + /// and will throw an when accessed from another thread. + /// It is possible to change the owner thread using the method. + /// + public void VerifyAccess() + { + if (Thread.CurrentThread != owner) + throw new InvalidOperationException("TextDocument can be accessed only from the thread that owns it."); + } + + /// + /// 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. + /// + /// + /// + /// + /// 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 . + /// + /// + 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 rope; + readonly DocumentLineTree lineTree; + readonly LineManager lineManager; + readonly TextAnchorTree anchorTree; + ChangeTrackingCheckpoint currentCheckpoint; + + /// + /// Create an empty text document. + /// + public TextDocument() + : this(string.Empty) + { + } + + /// + /// Create a new text document with the specified initial text. + /// + public TextDocument(IEnumerable initialText) + { + if (initialText == null) + throw new ArgumentNullException("initialText"); + rope = new Rope(initialText); + lineTree = new DocumentLineTree(this); + lineManager = new LineManager(lineTree, this); + lineTrackers.CollectionChanged += delegate { + lineManager.UpdateListOfLineTrackers(); + }; + + anchorTree = new TextAnchorTree(this); + undoStack = new UndoStack(); + FireChangeEvents(); + } + + /// + /// Create a new text document with the specified initial text. + /// + public TextDocument(ITextSource initialText) + : this(GetTextFromTextSource(initialText)) + { + } + + // gets the text from a text source, directly retrieving the underlying rope where possible + static IEnumerable 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)); + } + } + + /// + public string GetText(int offset, int length) + { + VerifyAccess(); + return rope.ToString(offset, length); + } + + /// + /// Retrieves the text for a portion of the document. + /// + 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); + } + + /// + public char GetCharAt(int offset) + { + DebugVerifyAccess(); // frequently called, so must be fast in release builds + return rope[offset]; + } + + WeakReference cachedText; + + /// + /// Gets/Sets the text of the whole document. + /// + 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); + } + } + + /// + /// + public event EventHandler TextChanged; + + /// + public int TextLength { + get { + VerifyAccess(); + return rope.Length; + } + } + + /// + /// Is raised when the TextLength property changes. + /// + /// + [Obsolete("This event will be removed in a future version; use the PropertyChanged event instead")] + public event EventHandler TextLengthChanged; + + /// + /// Is raised when one of the properties , , , + /// changes. + /// + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Is raised before the document changes. + /// + /// + /// Here is the order in which events are raised during a document update: + /// + /// BeginUpdate() + /// + /// Start of change group (on undo stack) + /// event is raised + /// + /// Insert() / Remove() / Replace() + /// + /// event is raised + /// The document is changed + /// TextAnchor.Deleted event is raised if anchors were + /// in the deleted text portion + /// event is raised + /// + /// EndUpdate() + /// + /// event is raised + /// event is raised (for the Text, TextLength, LineCount properties, in that order) + /// End of change group (on undo stack) + /// event is raised + /// + /// + /// + /// If the insert/remove/replace methods are called without a call to BeginUpdate(), + /// they will call BeginUpdate() and EndUpdate() to ensure no change happens outside of UpdateStarted/UpdateFinished. + /// + /// There can be multiple document changes between the BeginUpdate() and EndUpdate() calls. + /// In this case, the events associated with EndUpdate will be raised only once after the whole document update is done. + /// + /// The listens to the UpdateStarted and UpdateFinished events to group all changes into a single undo step. + /// + /// + public event EventHandler Changing; + + /// + /// Is raised after the document has changed. + /// + /// + public event EventHandler Changed; + + /// + /// Creates a snapshot of the current text. + /// + /// + /// 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. + /// + /// This special thread-safety guarantee is valid only for TextDocument.CreateSnapshot(), not necessarily for other + /// classes implementing ITextSource.CreateSnapshot(). + /// + /// + /// + public ITextSource CreateSnapshot() + { + lock (lockObject) { + return new RopeTextSource(rope.Clone()); + } + } + + /// + /// Creates a snapshot of the current text. + /// Additionally, creates a checkpoint that allows tracking document changes. + /// + /// + [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; + } + } + + /// + /// Creates a snapshot of a part of the current text. + /// + /// + public ITextSource CreateSnapshot(int offset, int length) + { + lock (lockObject) { + return new RopeTextSource(rope.GetRange(offset, length)); + } + } + + /// + public System.IO.TextReader CreateReader() + { + lock (lockObject) { + return new RopeTextReader(rope); + } + } + #endregion + + #region BeginUpdate / EndUpdate + int beginUpdateCount; + + /// + /// Gets if an update is running. + /// + /// + public bool IsInUpdate { + get { + VerifyAccess(); + return beginUpdateCount > 0; + } + } + + /// + /// Immediately calls , + /// and returns an IDisposable that calls . + /// + /// + public IDisposable RunUpdate() + { + BeginUpdate(); + return new CallbackOnDispose(EndUpdate); + } + + /// + /// Begins a group of document changes. + /// Some events are suspended until EndUpdate is called, and the will + /// group all changes into a single action. + /// Calling BeginUpdate several times increments a counter, only after the appropriate number + /// of EndUpdate calls the events resume their work. + /// + /// + 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); + } + } + + /// + /// Ends a group of document changes. + /// + /// + 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; + } + } + + /// + /// Occurs when a document change starts. + /// + /// + public event EventHandler UpdateStarted; + + /// + /// Occurs when a document change is finished. + /// + /// + public event EventHandler UpdateFinished; + #endregion + + #region Fire events after update + int oldTextLength; + int oldLineCount; + bool fireTextChanged; + + /// + /// Fires TextChanged, TextLengthChanged, LineCountChanged if required. + /// + 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 + /// + /// Inserts text. + /// + public void Insert(int offset, string text) + { + Replace(offset, 0, text); + } + + /// + /// Removes text. + /// + public void Remove(ISegment segment) + { + Replace(segment, string.Empty); + } + + /// + /// Removes text. + /// + public void Remove(int offset, int length) + { + Replace(offset, length, string.Empty); + } + + internal bool inDocumentChanging; + + /// + /// Replaces text. + /// + public void Replace(ISegment segment, string text) + { + if (segment == null) + throw new ArgumentNullException("segment"); + Replace(segment.Offset, segment.Length, text, null); + } + + /// + /// Replaces text. + /// + public void Replace(int offset, int length, string text) + { + Replace(offset, length, text, null); + } + + /// + /// Replaces text. + /// + /// The starting offset of the text to be replaced. + /// The length of the text to be replaced. + /// The new text. + /// 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. + 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"); + } + } + + /// + /// Replaces text. + /// + /// The starting offset of the text to be replaced. + /// The length of the text to be replaced. + /// The new text. + /// 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 . + /// Passing an OffsetChangeMap to the Replace method will automatically freeze it to ensure the thread safety of the resulting + /// DocumentChangeEventArgs instance. + /// + 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... + /// + /// Gets a read-only list of lines. + /// + /// + public IList Lines { + get { return lineTree; } + } + + /// + /// Gets a line by the line number: O(log n) + /// + 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); + } + + /// + /// Gets a document lines by offset. + /// Runtime: O(log n) + /// + [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 + + /// + /// Gets the offset from a text location. + /// + /// + public int GetOffset(TextLocation location) + { + return GetOffset(location.Line, location.Column); + } + + /// + /// Gets the offset from a text location. + /// + /// + 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; + } + + /// + /// Gets the location from an offset. + /// + /// + public TextLocation GetLocation(int offset) + { + DocumentLine line = GetLineByOffset(offset); + return new TextLocation(line.LineNumber, offset - line.Offset + 1); + } + + readonly ObservableCollection lineTrackers = new ObservableCollection(); + + /// + /// Gets the list of s attached to this document. + /// You can add custom line trackers to this list. + /// + public IList LineTrackers { + get { + VerifyAccess(); + return lineTrackers; + } + } + + UndoStack undoStack; + + /// + /// Gets the of the document. + /// + /// This property can also be used to set the undo stack, e.g. for sharing a common undo stack between multiple documents. + 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"); + } + } + } + + /// + /// Creates a new at the specified offset. + /// + /// + 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 + /// + /// Gets the total number of lines in the document. + /// Runtime: O(1). + /// + public int LineCount { + get { + VerifyAccess(); + return lineTree.LineCount; + } + } + + /// + /// Is raised when the LineCount property changes. + /// + [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(); + } + + /// + /// Gets the document lines tree in string form. + /// + [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 + } + + /// + /// Gets the text anchor tree in string form. + /// + [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 + } +} -- cgit v1.3.1