diff options
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors')
| -rw-r--r-- | Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs | 1695 | ||||
| -rw-r--r-- | Software/Visual_Studio/Scripting/Tango.Scripting.Editors/ScriptEditor.cs | 729 |
2 files changed, 1246 insertions, 1178 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs index 84fc86f44..a95d07fcf 100644 --- a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/TextDocument.cs @@ -14,823 +14,884 @@ 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 + /// <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(Math.Max(offset, 0), 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 +#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 - } +#endif + } + #endregion + } } diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/ScriptEditor.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/ScriptEditor.cs index efa1b087a..d06862cef 100644 --- a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/ScriptEditor.cs +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/ScriptEditor.cs @@ -279,18 +279,18 @@ namespace Tango.Scripting.Editors } else if (e.Key == Key.Oem2) { - int offset = CaretOffset; - var line = Document.GetLineByOffset(offset); + //int offset = CaretOffset; + //var line = Document.GetLineByOffset(offset); - String text = GetCurrentLineText(); - if (text.TrimStart('\t', ' ').StartsWith("//")) - { - Document.BeginUpdate(); - Document.Replace(line, "/// <summary>\n/// \n/// </summary>"); - Document.EndUpdate(); - e.Handled = true; - CaretOffset = Document.GetLineByNumber(line.LineNumber + 1).EndOffset; - } + //String text = GetCurrentLineText(); + //if (text.TrimStart('\t', ' ').StartsWith("//")) + //{ + // Document.BeginUpdate(); + // Document.Replace(line, "/// <summary>\n/// \n/// </summary>"); + // Document.EndUpdate(); + // e.Handled = true; + // CaretOffset = Document.GetLineByNumber(line.LineNumber + 1).EndOffset; + //} } else if (e.Key == Key.End || e.Key == Key.Home) { @@ -304,474 +304,481 @@ namespace Tango.Scripting.Editors private void TextArea_TextEntered(object sender, TextCompositionEventArgs e) { - List<Object> items = new List<object>(); - - HidePopup(); - - var lineText = GetCurrentLineText(); - var previousWords = GetPreviousWords(); - var previousWordsLast = previousWords.LastOrDefault(); - String currentWord = previousWordsLast != null ? previousWordsLast.Replace("\t", "") : String.Empty; - String currentWordIncludingParenthesis = currentWord.Split('(').LastOrDefault(); - - if (previousWords.Count > 0 && previousWords.First().Trim().StartsWith("//")) return; - - if (e.Text == " " && previousWords.Count > 2 && previousWords[previousWords.Count - 2] == "=") + try { - var expression = previousWords.First(); - var knownType = GetKnownTypeFromExpression(expression + "."); + List<Object> items = new List<object>(); - if (knownType != null && knownType.Type.IsEnum) - { - completionWindow.HideCompletion(); - IList<ICompletionData> data = new List<ICompletionData>(); + HidePopup(); - foreach (var field in knownType.Fields) - { - data.Add(new FieldCompletionItem() - { - Class = knownType.FriendlyName, - Name = knownType.FriendlyName + "." + field.Name, - Type = field.ReturnTypeFriendlyName, - Description = field.Summary, - }); - } + var lineText = GetCurrentLineText(); + var previousWords = GetPreviousWords(); + var previousWordsLast = previousWords.LastOrDefault(); + String currentWord = previousWordsLast != null ? previousWordsLast.Replace("\t", "") : String.Empty; + String currentWordIncludingParenthesis = currentWord.Split('(').LastOrDefault(); - ShowCompletionWindow(data.OrderBy(x => x.Text).ToList(), GetCurrentWord()); - } - - } - else if (e.Text == " " && GetPreviousWord() == "new") - { - var s = _parser.GetExpressionFirst<FieldDeclarationSyntax>(GetCurrentLineText()); - - if (s != null) - { - String type = s.Declaration.Type.ToString(); - - IList<ICompletionData> data = new List<ICompletionData>(); - - data.Add(new ClassCompletionItem() - { - Name = type, - Description = "Auto generate assignment...", - }); + if (previousWords.Count > 0 && previousWords.First().Trim().StartsWith("//")) return; - ShowCompletionWindow(data, type); - } - } - else if (e.Text == ";" || e.Text == " ") - { - HideCompletionWindow(); - } - else if (e.Text == ".") - { - var knownType = GetCurrentKnownType(); - - if (knownType != null) + if (e.Text == " " && previousWords.Count > 2 && previousWords[previousWords.Count - 2] == "=") { - completionWindow.HideCompletion(); - IList<ICompletionData> data = new List<ICompletionData>(); + var expression = previousWords.First(); + var knownType = GetKnownTypeFromExpression(expression + "."); - if (!knownType.Type.IsEnum) + if (knownType != null && knownType.Type.IsEnum) { - var typeMembers = knownType.Members.ToList(); + completionWindow.HideCompletion(); + IList<ICompletionData> data = new List<ICompletionData>(); - foreach (var methodGroup in typeMembers.OfType<KnownTypeMethod>().GroupBy(x => x.NameWithTypeArguments)) + foreach (var field in knownType.Fields) { - var method = methodGroup.First(); - - data.Add(new MethodCompletionItem() + data.Add(new FieldCompletionItem() { Class = knownType.FriendlyName, - Name = method.NameWithTypeArguments, - ReturnType = method.ReturnTypeFriendlyName, - Description = method.Summary, - Parameters = method.Parameters, - Overloads = methodGroup.Count() - 1, + Name = knownType.FriendlyName + "." + field.Name, + Type = field.ReturnTypeFriendlyName, + Description = field.Summary, }); } - foreach (var methodGroup in typeMembers.Where(x => x.GetType() != typeof(KnownTypeMethod)).GroupBy(x => x.Name)) - { - var member = methodGroup.First(); + ShowCompletionWindow(data.OrderBy(x => x.Text).ToList(), GetCurrentWord()); + } - data.Add(new PropertyCompletionItem() - { - Class = knownType.FriendlyName, - Name = member.Name, - Type = member.ReturnTypeFriendlyName, - Description = member.Summary, - }); + } + else if (e.Text == " " && GetPreviousWord() == "new") + { + var s = _parser.GetExpressionFirst<FieldDeclarationSyntax>(GetCurrentLineText()); - } - } - else + if (s != null) { - foreach (var field in knownType.Fields) + String type = s.Declaration.Type.ToString(); + + IList<ICompletionData> data = new List<ICompletionData>(); + + data.Add(new ClassCompletionItem() { - data.Add(new FieldCompletionItem() - { - Class = knownType.FriendlyName, - Name = field.Name, - Type = field.ReturnTypeFriendlyName, - Description = field.Summary, - }); + Name = type, + Description = "Auto generate assignment...", + }); - } + ShowCompletionWindow(data, type); } - - ShowCompletionWindow(data.OrderBy(x => x.Text).ToList(), GetCurrentWord()); } - else + else if (e.Text == ";" || e.Text == " ") + { + HideCompletionWindow(); + } + else if (e.Text == ".") { - var declaredType = GetCurrentDeclaredType(); + var knownType = GetCurrentKnownType(); - if (declaredType != null) + if (knownType != null) { completionWindow.HideCompletion(); IList<ICompletionData> data = new List<ICompletionData>(); - var typeMembers = declaredType.Symbols.ToList(); - - foreach (var methodGroup in typeMembers.GroupBy(x => x.Name)) + if (!knownType.Type.IsEnum) { - var member = methodGroup.First(); + var typeMembers = knownType.Members.ToList(); - if (member.Kind == SymbolKind.Method) + foreach (var methodGroup in typeMembers.OfType<KnownTypeMethod>().GroupBy(x => x.NameWithTypeArguments)) { - var methodCompletion = new MethodCompletionItem() - { - Class = declaredType.Name, - Name = member.Name, - ReturnType = member.Type, - Description = member.Summary, - Overloads = methodGroup.Count() - 1, - }; + var method = methodGroup.First(); - - for (int i = 0; i < member.Parameters.Count; i++) + data.Add(new MethodCompletionItem() { - var pair = member.Parameters[i]; - - methodCompletion.Parameters.Add(new KnownTypeMethodParameter() - { - Type = pair.Key, - Name = pair.Value, - IsLast = (i == member.Parameters.Count - 1) - }); - } - - data.Add(methodCompletion); - + Class = knownType.FriendlyName, + Name = method.NameWithTypeArguments, + ReturnType = method.ReturnTypeFriendlyName, + Description = method.Summary, + Parameters = method.Parameters, + Overloads = methodGroup.Count() - 1, + }); } - else if (member.Kind == SymbolKind.Property) + + foreach (var methodGroup in typeMembers.Where(x => x.GetType() != typeof(KnownTypeMethod)).GroupBy(x => x.Name)) { + var member = methodGroup.First(); + data.Add(new PropertyCompletionItem() { - Class = declaredType.Name, + Class = knownType.FriendlyName, Name = member.Name, - Type = member.Type, + Type = member.ReturnTypeFriendlyName, Description = member.Summary, }); + } - else if (member.Kind == SymbolKind.Field) + } + else + { + foreach (var field in knownType.Fields) { data.Add(new FieldCompletionItem() { - Class = declaredType.Name, - Name = member.Name, - Type = member.Type, - Description = member.Summary, + Class = knownType.FriendlyName, + Name = field.Name, + Type = field.ReturnTypeFriendlyName, + Description = field.Summary, }); + } } - ShowCompletionWindow(data, GetCurrentWord()); + ShowCompletionWindow(data.OrderBy(x => x.Text).ToList(), GetCurrentWord()); } - } - } - else if (e.Text == "(" || e.Text == ",") - { - completionWindow.HideCompletion(); + else + { + var declaredType = GetCurrentDeclaredType(); - try + if (declaredType != null) + { + completionWindow.HideCompletion(); + IList<ICompletionData> data = new List<ICompletionData>(); + + var typeMembers = declaredType.Symbols.ToList(); + + foreach (var methodGroup in typeMembers.GroupBy(x => x.Name)) + { + var member = methodGroup.First(); + + if (member.Kind == SymbolKind.Method) + { + var methodCompletion = new MethodCompletionItem() + { + Class = declaredType.Name, + Name = member.Name, + ReturnType = member.Type, + Description = member.Summary, + Overloads = methodGroup.Count() - 1, + }; + + + for (int i = 0; i < member.Parameters.Count; i++) + { + var pair = member.Parameters[i]; + + methodCompletion.Parameters.Add(new KnownTypeMethodParameter() + { + Type = pair.Key, + Name = pair.Value, + IsLast = (i == member.Parameters.Count - 1) + }); + } + + data.Add(methodCompletion); + + } + else if (member.Kind == SymbolKind.Property) + { + data.Add(new PropertyCompletionItem() + { + Class = declaredType.Name, + Name = member.Name, + Type = member.Type, + Description = member.Summary, + }); + } + else if (member.Kind == SymbolKind.Field) + { + data.Add(new FieldCompletionItem() + { + Class = declaredType.Name, + Name = member.Name, + Type = member.Type, + Description = member.Summary, + }); + } + } + + ShowCompletionWindow(data, GetCurrentWord()); + } + } + } + else if (e.Text == "(" || e.Text == ",") { - var session = GetConstructionSession(); + completionWindow.HideCompletion(); - if (session != null) + try { - var content = CreateConstructionSessionPopupContent(session); - if (content.Methods.Count > 0) + var session = GetConstructionSession(); + + if (session != null) { - ShowPopup(content); - return; + var content = CreateConstructionSessionPopupContent(session); + if (content.Methods.Count > 0) + { + ShowPopup(content); + return; + } } - } - var methodSession = GetMethodSession(); + var methodSession = GetMethodSession(); - if (methodSession != null) - { - var content = CreateMethodSessionPopupContent(methodSession); - if (content.Methods.Count > 0) + if (methodSession != null) { - ShowPopup(content); - return; + var content = CreateMethodSessionPopupContent(methodSession); + if (content.Methods.Count > 0) + { + ShowPopup(content); + return; + } } - } - var declaredMethodSession = GetDeclaredMethodSession(); + var declaredMethodSession = GetDeclaredMethodSession(); - if (declaredMethodSession != null) - { - var content = CreateDeclaredMethodSessionPopupContent(declaredMethodSession); - if (content.Methods.Count > 0) + if (declaredMethodSession != null) { - ShowPopup(content); - return; + var content = CreateDeclaredMethodSessionPopupContent(declaredMethodSession); + if (content.Methods.Count > 0) + { + ShowPopup(content); + return; + } } } + catch (Exception ex) + { + Debug.WriteLine(ex); + } } - catch (Exception ex) - { - Debug.WriteLine(ex); - } - } - else if (lineText.StartsWith("using")) - { - if (completionWindow.IsVisible) + else if (lineText.StartsWith("using") && e.Text != "\n") { - completionWindow.UpdatePositionFix(); - return; - } + if (completionWindow.IsVisible) + { + completionWindow.UpdatePositionFix(); + return; + } - IList<ICompletionData> data = new List<ICompletionData>(); + IList<ICompletionData> data = new List<ICompletionData>(); - foreach (var asm in ReferenceAssemblies) - { - foreach (var ns in asm.Assembly.GetTypes().Select(x => x.Namespace).Distinct().Where(x => x != null)) + foreach (var asm in ReferenceAssemblies) { - data.Add(new NamespaceCompletionItem() + foreach (var ns in asm.Assembly.GetTypes().Select(x => x.Namespace).Distinct().Where(x => x != null)) { - Name = ns, - Assembly = asm.Assembly.GetName().Name, - }); + data.Add(new NamespaceCompletionItem() + { + Name = ns, + Assembly = asm.Assembly.GetName().Name, + }); + } } - } - data = data.DistinctBy(x => x.Text).ToList(); + data = data.DistinctBy(x => x.Text).ToList(); - ShowCompletionWindow(data, GetCurrentWord()); - } - else if (e.Text == "{") - { - int parentesisCount = lineText.TakeWhile(x => x != '{').Count(x => x == '\"'); - - if (parentesisCount % 2 == 0) - { - Document.Insert(CaretOffset, "}"); - CaretOffset--; + ShowCompletionWindow(data, GetCurrentWord()); } - } - else if (e.Text == "}") - { - if (Document.GetText(CaretOffset - 2, 1) == "{" && Document.GetText(CaretOffset, 1) == "}") - { - Document.Replace(CaretOffset, 1, ""); - } - } - else if (e.Text == "\n") - { - if (Document.GetText(CaretOffset - 3, 1) == "{" && Document.GetText(CaretOffset, 1) == "}") + else if (e.Text == "{") { - CaretOffset--; - Document.Insert(CaretOffset, "\n\t"); + int parentesisCount = lineText.TakeWhile(x => x != '{').Count(x => x == '\"'); + + if (parentesisCount % 2 == 0) + { + Document.Insert(CaretOffset, "}"); + CaretOffset--; + } } - } - else if (!currentWordIncludingParenthesis.Contains(".") || currentWord[currentWord.Length - 2] == '<') - { - if (completionWindow.IsVisible) + else if (e.Text == "}") { - completionWindow.UpdatePositionFix(); - return; + if (Document.GetText(CaretOffset - 2, 1) == "{" && Document.GetText(CaretOffset, 1) == "}") + { + Document.Replace(CaretOffset, 1, ""); + } } - - var previous_word = GetPreviousWord(); - var word = GetCurrentWord(); - - if (word.Contains("<")) + else if (e.Text == "\n") { - word = word.Last(x => x != '<').ToString(); + if (Document.GetText(CaretOffset - 3, 1) == "{" && Document.GetText(CaretOffset, 1) == "}") + { + CaretOffset--; + Document.Insert(CaretOffset, "\n\t"); + } } - - if (previous_word != word) + else if (!currentWordIncludingParenthesis.Contains(".") || currentWord[currentWord.Length - 2] == '<') { - if (_knownTypes.Exists(x => x.Name == previous_word)) + if (completionWindow.IsVisible) { + completionWindow.UpdatePositionFix(); return; } - if (_blocking_type_words.Contains(previous_word)) + var previous_word = GetPreviousWord(); + var word = GetCurrentWord(); + + if (word.Contains("<")) { - return; + word = word.Last(x => x != '<').ToString(); } - } - - if (!String.IsNullOrWhiteSpace(word)) - { - IList<ICompletionData> data = new List<ICompletionData>(); - foreach (var type in _declaredTypes.Where(x => x.Name.StartsWith(word))) + if (previous_word != word) { - if (type.Kind == TypeKind.Struct) + if (_knownTypes.Exists(x => x.Name == previous_word)) { - data.Add(new StructCompletionItem() - { - Name = type.Name, - Description = type.Summary, - Namespace = type.ContainingNamespace, - Priority = 1, - }); - } - else if (type.Kind == TypeKind.Enum) - { - data.Add(new EnumCompletionItem() - { - Name = type.Name, - Description = type.Summary, - Namespace = type.ContainingNamespace, - Priority = 1, - }); - } - else if (type.Kind == TypeKind.Interface) - { - data.Add(new InterfaceCompletionItem() - { - Name = type.Name, - Description = type.Summary, - Namespace = type.ContainingNamespace, - Priority = 1, - }); - } - else if (type.Kind == TypeKind.Class) - { - data.Add(new ClassCompletionItem() - { - Name = type.Name, - Description = type.Summary, - Namespace = type.ContainingNamespace, - Priority = 1, - }); + return; } - else + + if (_blocking_type_words.Contains(previous_word)) { - throw new NotImplementedException("Implement generic item here!"); + return; } } - foreach (var type in _knownTypes.ToList().Where(x => x.Name.StartsWith(word))) + if (!String.IsNullOrWhiteSpace(word)) { - if (type.Type.IsEnum) + IList<ICompletionData> data = new List<ICompletionData>(); + + foreach (var type in _declaredTypes.Where(x => x.Name.StartsWith(word))) { - data.Add(new EnumCompletionItem() + if (type.Kind == TypeKind.Struct) { - Namespace = type.Type.Namespace, - Description = type.Summary, - Name = type.FriendlyName, - Priority = 0, - }); - } - else if (type.Type.IsInterface) - { - data.Add(new InterfaceCompletionItem() + data.Add(new StructCompletionItem() + { + Name = type.Name, + Description = type.Summary, + Namespace = type.ContainingNamespace, + Priority = 1, + }); + } + else if (type.Kind == TypeKind.Enum) { - Name = type.FriendlyName, - Description = type.Summary, - Namespace = type.Type.Namespace, - Priority = 0, - }); - } - else if (type.Type.IsValueType) - { - data.Add(new StructCompletionItem() + data.Add(new EnumCompletionItem() + { + Name = type.Name, + Description = type.Summary, + Namespace = type.ContainingNamespace, + Priority = 1, + }); + } + else if (type.Kind == TypeKind.Interface) { - Name = type.FriendlyName, - Description = type.Summary, - Namespace = type.Type.Namespace, - Priority = 0, - }); - } - else if (type.Type.IsClass) - { - data.Add(new ClassCompletionItem() + data.Add(new InterfaceCompletionItem() + { + Name = type.Name, + Description = type.Summary, + Namespace = type.ContainingNamespace, + Priority = 1, + }); + } + else if (type.Kind == TypeKind.Class) { - Name = type.FriendlyName, - Description = type.Summary, - Namespace = type.Type.Namespace, - Priority = 0, - }); - } - else - { - throw new NotImplementedException("Implement generic item here."); + data.Add(new ClassCompletionItem() + { + Name = type.Name, + Description = type.Summary, + Namespace = type.ContainingNamespace, + Priority = 1, + }); + } + else + { + throw new NotImplementedException("Implement generic item here!"); + } } - } - foreach (var symbol in _parser.GetContextSymbols(Document.Text, CaretOffset).Where(x => x.Name.StartsWith(GetCurrentWord()))) - { - if (symbol.Kind == SymbolKind.Property) + foreach (var type in _knownTypes.ToList().Where(x => x.Name.StartsWith(word))) { - data.Add(new PropertyCompletionItem() + if (type.Type.IsEnum) { - Class = symbol.Class, - Description = symbol.Summary, - Name = symbol.Name, - Type = symbol.Type, - Priority = 2, - }); - } - else if (symbol.Kind == SymbolKind.Field || symbol.Kind == SymbolKind.Local || symbol.Kind == SymbolKind.Parameter) - { - data.Add(new FieldCompletionItem() + data.Add(new EnumCompletionItem() + { + Namespace = type.Type.Namespace, + Description = type.Summary, + Name = type.FriendlyName, + Priority = 0, + }); + } + else if (type.Type.IsInterface) { - Class = symbol.Class, - Description = symbol.Summary, - Name = symbol.Name, - Type = symbol.Type, - Priority = 2, - }); + data.Add(new InterfaceCompletionItem() + { + Name = type.FriendlyName, + Description = type.Summary, + Namespace = type.Type.Namespace, + Priority = 0, + }); + } + else if (type.Type.IsValueType) + { + data.Add(new StructCompletionItem() + { + Name = type.FriendlyName, + Description = type.Summary, + Namespace = type.Type.Namespace, + Priority = 0, + }); + } + else if (type.Type.IsClass) + { + data.Add(new ClassCompletionItem() + { + Name = type.FriendlyName, + Description = type.Summary, + Namespace = type.Type.Namespace, + Priority = 0, + }); + } + else + { + throw new NotImplementedException("Implement generic item here."); + } } - else if (symbol.Kind == SymbolKind.Method) + + foreach (var symbol in _parser.GetContextSymbols(Document.Text, CaretOffset).Where(x => x.Name.StartsWith(GetCurrentWord()))) { - var methodCompletion = new MethodCompletionItem() + if (symbol.Kind == SymbolKind.Property) { - Class = symbol.Class, - Description = symbol.Summary, - Name = symbol.Name, - ReturnType = symbol.Type, - Priority = 2, - }; - - for (int i = 0; i < symbol.Parameters.Count; i++) + data.Add(new PropertyCompletionItem() + { + Class = symbol.Class, + Description = symbol.Summary, + Name = symbol.Name, + Type = symbol.Type, + Priority = 2, + }); + } + else if (symbol.Kind == SymbolKind.Field || symbol.Kind == SymbolKind.Local || symbol.Kind == SymbolKind.Parameter) { - var pair = symbol.Parameters[i]; - - methodCompletion.Parameters.Add(new KnownTypeMethodParameter() + data.Add(new FieldCompletionItem() { - Type = pair.Key, - Name = pair.Value, - IsLast = (i == symbol.Parameters.Count - 1) + Class = symbol.Class, + Description = symbol.Summary, + Name = symbol.Name, + Type = symbol.Type, + Priority = 2, }); } + else if (symbol.Kind == SymbolKind.Method) + { + var methodCompletion = new MethodCompletionItem() + { + Class = symbol.Class, + Description = symbol.Summary, + Name = symbol.Name, + ReturnType = symbol.Type, + Priority = 2, + }; + + for (int i = 0; i < symbol.Parameters.Count; i++) + { + var pair = symbol.Parameters[i]; + + methodCompletion.Parameters.Add(new KnownTypeMethodParameter() + { + Type = pair.Key, + Name = pair.Value, + IsLast = (i == symbol.Parameters.Count - 1) + }); + } - data.Add(methodCompletion); + data.Add(methodCompletion); + } } - } - ShowCompletionWindow(data, word); + ShowCompletionWindow(data, word); + } } } + catch (Exception ex) + { + Debug.WriteLine(ex.ToString()); + } } #endregion |
