public TextChangeContext(EditorTree editorTree, TextChangeEventArgs change, TextChange pendingChanges) { EditorTree = editorTree; NewStart = change.Start; OldStart = change.OldStart; OldLength = change.OldLength; NewLength = change.NewLength; OldTextProvider = change.OldText != null ? change.OldText : editorTree.AstRoot.TextProvider; NewTextProvider = change.NewText != null ? change.NewText : new TextProvider(editorTree.TextBuffer.CurrentSnapshot, partial: true); PendingChanges = pendingChanges; TextChange textChange = new TextChange(); textChange.OldRange = this.OldRange; textChange.OldTextProvider = this.OldTextProvider; textChange.NewRange = this.NewRange; textChange.NewTextProvider = this.NewTextProvider; textChange.Version = this.NewTextProvider.Version; PendingChanges.Combine(textChange); }
public static EditorTree ApplyTextChange(string expression, int start, int oldLength, int newLength, string newText) { TextBufferMock textBuffer = new TextBufferMock(expression, RContentTypeDefinition.ContentType); EditorTree tree = new EditorTree(textBuffer); tree.Build(); TextChange tc = new TextChange(); tc.OldRange = new TextRange(start, oldLength); tc.NewRange = new TextRange(start, newLength); tc.OldTextProvider = new TextProvider(textBuffer.CurrentSnapshot); if (oldLength == 0 && newText.Length > 0) { textBuffer.Insert(start, newText); } else if (oldLength > 0 && newText.Length > 0) { textBuffer.Replace(new Span(start, oldLength), newText); } else { textBuffer.Delete(new Span(start, oldLength)); } return tree; }
/// <summary> /// Processes a single text change incrementally. Enqueues resulting /// tree changes in the supplied queue. Does not modify the tree. /// Changes are to be sent to the main thread and applied from there. /// Caller is responsible for the tree read lock acquisition. /// </summary> /// <param name="start">Start position of the change</param> /// <param name="oldLength">Length of the original text (0 if insertion)</param> /// <param name="newLength">Length of the new text (0 if deletion)</param> /// <param name="oldSnapshot">Text snapshot before the change</param> /// <param name="newSnapshot">Text snapshot after the change</param> /// <param name="treeChanges">Collection of tree changes to apply /// from the main thread</param> public void ProcessChange(TextChange textChange, EditorTreeChangeCollection treeChanges) { IAstNode startNode = null, endNode = null; PositionType startPositionType = PositionType.Undefined; PositionType endPositionType = PositionType.Undefined; IAstNode commonParent = null; int start = textChange.OldRange.Start; int oldLength = textChange.OldRange.Length; int newLength = textChange.NewRange.Length; int offset = newLength - oldLength; ITextProvider oldSnapshot = textChange.OldTextProvider; ITextProvider newSnapshot = textChange.NewTextProvider; // Find position type and the enclosing element node. Note that element // positions have been adjusted already (it happens immediately in OnTextChange) // so we should be looking at the new range even that tree hasn't // been fully updated yet. For example,if we delete a node, subsequent // elements were already shifted up and damaged nodes have been removed // so current node positions reflect text buffer state after the change. _astRoot.GetElementsEnclosingRange(start, newLength, out startNode, out startPositionType, out endNode, out endPositionType); if (startNode is AstRoot) { commonParent = _astRoot; } else if (startNode == endNode) { if (startPositionType == PositionType.Token) { // Change in comment or string content. commonParent = OnTokenNodeChange(startNode as TokenNode, start, oldLength, newLength); } } else { //if (commonParent == null) //{ // // Find parent that still has well formed curly braces. // commonParent = FindWellFormedOuterScope(startNode); //} if (commonParent == null) { commonParent = _astRoot; } } if (IsCancellationRequested()) return; if (!(commonParent is AstRoot)) { Debug.Assert(commonParent is IScope); AstRoot subTree = RParser.Parse(newSnapshot, commonParent); return; } AstRoot newTree = RParser.Parse(newSnapshot); treeChanges.ChangeQueue.Enqueue(new EditorTreeChange_NewTree(newTree)); }
public static void ApplyTextChange(ITextBuffer textBuffer, int start, int oldLength, int newLength, string newText) { TextChange tc = new TextChange(); tc.OldRange = new TextRange(start, oldLength); tc.NewRange = new TextRange(start, newLength); tc.OldTextProvider = new TextProvider(textBuffer.CurrentSnapshot); if (oldLength == 0 && newText.Length > 0) { textBuffer.Insert(start, newText); } else if (oldLength > 0 && newText.Length > 0) { textBuffer.Replace(new Span(start, oldLength), newText); } else { textBuffer.Delete(new Span(start, oldLength)); } }
/// <summary> /// Main asyncronous task body /// </summary> void ProcessTextChanges(TextChange changeToProcess, bool async, Func<bool> isCancelledCallback) { lock (_disposeLock) { if (_editorTree == null || _disposed || isCancelledCallback()) return; EditorTreeChangeCollection treeChanges = null; // Cache id since it can change if task is canceled long taskId = TaskId; try { AstRoot rootNode; // We only need read lock since changes will be applied // from the main thread if (async) { rootNode = _editorTree.AcquireReadLock(_treeUserId); } else { rootNode = _editorTree.GetAstRootUnsafe(); } treeChanges = new EditorTreeChangeCollection(changeToProcess.Version, changeToProcess.FullParseRequired); TextChangeProcessor changeProcessor = new TextChangeProcessor(_editorTree, rootNode, isCancelledCallback); bool fullParseRequired = changeToProcess.FullParseRequired; if (fullParseRequired) { changeProcessor.FullParse(treeChanges, changeToProcess.NewTextProvider); } else { changeProcessor.ProcessChange(changeToProcess, treeChanges); } } finally { if (async && _editorTree != null) _editorTree.ReleaseReadLock(_treeUserId); } // Lock should be released at this point since actual application // of tree changes is going to be happen from the main thread. if (!isCancelledCallback() && treeChanges != null) { // Queue results for the main thread application. This must be done before // signaling that the task is complete since if EnsureProcessingComplete // is waiting it will want to apply changes itself rather than wait for // the DispatchOnUIThread to go though and hence it will need all changes // stored and ready for application. _backgroundParsingResults.Enqueue(treeChanges); } // Signal task complete now so if main thread is waiting // it can proceed and appy the changes immediately. SignalTaskComplete(taskId); if (_backgroundParsingResults.Count > 0) { _uiThreadTransitionRequestTime = DateTime.UtcNow; // It is OK to post results while main thread might be working // on them since if if it does, by the time posted request comes // queue will already be empty. if (async) { // Post request to apply tree changes to the main thread. // This must NOT block or else task will never enter 'RanToCompletion' state. EditorShell.DispatchOnUIThread(() => ApplyBackgroundProcessingResults()); } else { // When processing is synchronous, apply changes and fire events right away. ApplyBackgroundProcessingResults(); } } } }
/// <summary> /// Processes text buffer changed accumulated so far. /// Typically called on idle. /// </summary> /// <param name="newTextProvider">New text buffer content</param> /// <param name="async">True if processing is to be done asynchronously. /// Non-async processing is typically used in unit tests only.</param> internal void ProcessPendingTextBufferChanges(ITextProvider newTextProvider, bool async) { if (Thread.CurrentThread.ManagedThreadId != _ownerThreadId) throw new ThreadStateException("Method should only be called on the main thread"); if (ChangesPending) { if (async && (IsTaskRunning() || _backgroundParsingResults.Count > 0)) { // Try next time or we may end up spawning a lot of tasks return; } // Combine changes in processing with pending changes. var changesToProcess = new TextChange(_pendingChanges, newTextProvider); // We need to signal task start here, in the main thread since it takes // some time before task is created and when it actually starts. // Therefore setting task state in the task body creates a gap // where we may end up spawning another task. base.Run((isCancelledCallback) => ProcessTextChanges(changesToProcess, async, isCancelledCallback), async); } }
/// <summary> /// Handles non-trivial changes like changes that delete elements, /// change identifier names, introducing new braces: changes /// that cannot be handled without background parse. /// </summary> private void ProcessComplexChange(TextChangeContext context) { // Cancel background parse if it is running Cancel(); TextChange textChange = new TextChange() { OldTextProvider = context.OldTextProvider, NewTextProvider = context.NewTextProvider }; try { // Get write lock since there may be concurrent readers // of the tree. Note that there are no concurrent writers // since changes can only come from a background parser // and are always applied from the main thread. _editorTree.AcquireWriteLock(); if (_pendingChanges.FullParseRequired) { // When full parse is required, change is like replace the entire file textChange.OldRange = TextRange.FromBounds(0, context.OldText.Length); textChange.NewRange = TextRange.FromBounds(0, context.NewText.Length); // Remove all elements from the tree _editorTree.Invalidate(); } else { textChange.OldRange = context.OldRange; textChange.NewRange = context.NewRange; DeleteAndShiftElements(context); Debug.Assert(_editorTree.AstRoot.Children.Count > 0); } _pendingChanges.Combine(textChange); _pendingChanges.Version = TextBuffer != null ? TextBuffer.CurrentSnapshot.Version.VersionNumber : 1; UpdateTreeTextSnapshot(); } finally { // Lock must be released before firing events otherwise we may hang _editorTree.ReleaseWriteLock(); } _editorTree.FireOnUpdateCompleted(TreeUpdateType.NodesRemoved); }
public void TextChange_Test() { TextChange tc = new TextChange(); tc.IsEmpty.Should().BeTrue(); tc.TextChangeType.Should().Be(TextChangeType.Trivial); string content = "23456789"; TextBufferMock textBuffer = new TextBufferMock(content, RContentTypeDefinition.ContentType); ITextSnapshot oldSnapshot = textBuffer.CurrentSnapshot; textBuffer.Insert(0, "1"); ITextSnapshot newSnapshot1 = textBuffer.CurrentSnapshot; textBuffer.Insert(0, "0"); ITextSnapshot newSnapshot2 = textBuffer.CurrentSnapshot; tc.OldTextProvider = new TextProvider(oldSnapshot); tc.NewTextProvider = new TextProvider(newSnapshot1); tc.OldRange = new TextRange(0, 0); tc.NewRange = new TextRange(0, 1); var tc1 = new TextChange(tc, new TextProvider(newSnapshot2)); tc1.ShouldBeEquivalentTo(new { OldRange = new { Length = 0 }, NewRange = new { Length = 2 }, Version = 2, FullParseRequired = false, IsEmpty = false, IsSimpleChange = true }, o => o.ExcludingMissingMembers()); var tc2 = tc1.Clone() as TextChange; tc2.ShouldBeEquivalentTo(new { OldRange = new { Length = 0 }, NewRange = new { Length = 2 }, Version = 2, FullParseRequired = false, IsEmpty = false, IsSimpleChange = true }, o => o.ExcludingMissingMembers()); tc1.Clear(); tc1.ShouldBeEquivalentTo(new { OldRange = new { Length = 0 }, NewRange = new { Length = 0 }, OldTextProvider = (ITextProvider)null, NewTextProvider = (ITextProvider)null, IsEmpty = true, IsSimpleChange = true }, o => o.ExcludingMissingMembers()); tc2.ShouldBeEquivalentTo(new { OldRange = new { Length = 0 }, NewRange = new { Length = 2 }, Version = 2, FullParseRequired = false, IsEmpty = false, IsSimpleChange = true }, o => o.ExcludingMissingMembers()); }
/// <summary> /// Combines one text change with another /// </summary> public void Combine(TextChange other) { if (other.IsEmpty) { return; } FullParseRequired |= other.FullParseRequired; TextChangeType |= other.TextChangeType; if (OldRange == TextRange.EmptyRange || NewRange == TextRange.EmptyRange) { OldRange = other.OldRange; NewRange = other.NewRange; OldTextProvider = other.OldTextProvider; NewTextProvider = other.NewTextProvider; } else { ITextSnapshotProvider oldSnapshotProvider = OldTextProvider as ITextSnapshotProvider; ITextSnapshotProvider newSnapshotProvider = NewTextProvider as ITextSnapshotProvider; ITextSnapshotProvider otherOldSnapshotProvider = other.OldTextProvider as ITextSnapshotProvider; ITextSnapshotProvider otherNewSnapshotProvider = other.NewTextProvider as ITextSnapshotProvider; bool changesAreFromNextSnapshot = false; if ((oldSnapshotProvider != null) && (newSnapshotProvider != null) && (otherOldSnapshotProvider != null) && (otherNewSnapshotProvider != null)) { ITextSnapshot newSnapshot = newSnapshotProvider.Snapshot; ITextSnapshot otherOldSnapshot = otherOldSnapshotProvider.Snapshot; if (newSnapshot.Version.ReiteratedVersionNumber == otherOldSnapshot.Version.ReiteratedVersionNumber) { changesAreFromNextSnapshot = true; } } if (!changesAreFromNextSnapshot) { // Assume these changes are from the same snapshot int oldStart = Math.Min(other.OldRange.Start, this.OldRange.Start); int oldEnd = Math.Max(other.OldRange.End, this.OldRange.End); int newStart = Math.Min(other.NewRange.Start, this.NewRange.Start); int newEnd = Math.Max(other.NewRange.End, this.NewRange.End); OldRange = TextRange.FromBounds(oldStart, oldEnd); NewRange = TextRange.FromBounds(newStart, newEnd); } else { // the "other" change is from the subsequent snapshot. Merge the changes. ITextSnapshot oldSnapshot = oldSnapshotProvider.Snapshot; ITextSnapshot newSnapshot = newSnapshotProvider.Snapshot; ITextSnapshot otherOldSnapshot = otherOldSnapshotProvider.Snapshot; ITextSnapshot otherNewSnapshot = otherNewSnapshotProvider.Snapshot; SnapshotSpan otherOldSpan = other.OldRange.ToSnapshotSpan(otherOldSnapshot); SnapshotSpan oldSpan = otherOldSpan.TranslateTo(oldSnapshot, SpanTrackingMode.EdgeInclusive); SnapshotSpan newSpan = NewRange.ToSnapshotSpan(newSnapshot); SnapshotSpan otherNewSpan = newSpan.TranslateTo(otherNewSnapshot, SpanTrackingMode.EdgeInclusive); OldRange = new TextRange(TextRange.Union(OldRange, oldSpan.ToTextRange())); NewRange = new TextRange(TextRange.Union(other.NewRange, otherNewSpan.ToTextRange())); NewTextProvider = other.NewTextProvider; } } Version = Math.Max(this.Version, other.Version); }
/// <summary> /// Processes a single text change incrementally. Enqueues resulting /// tree changes in the supplied queue. Does not modify the tree. /// Changes are to be sent to the main thread and applied from there. /// Caller is responsible for the tree read lock acquisition. /// </summary> /// <param name="start">Start position of the change</param> /// <param name="oldLength">Length of the original text (0 if insertion)</param> /// <param name="newLength">Length of the new text (0 if deletion)</param> /// <param name="oldSnapshot">Text snapshot before the change</param> /// <param name="newSnapshot">Text snapshot after the change</param> /// <param name="treeChanges">Collection of tree changes to apply /// from the main thread</param> public void ProcessChange(TextChange textChange, EditorTreeChangeCollection treeChanges) { IAstNode startNode = null, endNode = null; PositionType startPositionType = PositionType.Undefined; PositionType endPositionType = PositionType.Undefined; IAstNode commonParent = null; int start = textChange.OldRange.Start; int oldLength = textChange.OldRange.Length; int newLength = textChange.NewRange.Length; int offset = newLength - oldLength; ITextProvider oldSnapshot = textChange.OldTextProvider; ITextProvider newSnapshot = textChange.NewTextProvider; // Find position type and the enclosing element node. Note that element // positions have been adjusted already (it happens immediately in OnTextChange) // so we should be looking at the new range even that tree hasn't // been fully updated yet. For example,if we delete a node, subsequent // elements were already shifted up and damaged nodes have been removed // so current node positions reflect text buffer state after the change. _astRoot.GetElementsEnclosingRange(start, newLength, out startNode, out startPositionType, out endNode, out endPositionType); if (startNode is AstRoot) { commonParent = _astRoot; } else if (startNode == endNode) { if (startPositionType == PositionType.Token) { // Change in comment or string content. commonParent = OnTokenNodeChange(startNode as TokenNode, start, oldLength, newLength); } } else { //if (commonParent == null) //{ // // Find parent that still has well formed curly braces. // commonParent = FindWellFormedOuterScope(startNode); //} if (commonParent == null) { commonParent = _astRoot; } } if (IsCancellationRequested()) { return; } if (!(commonParent is AstRoot)) { Debug.Assert(commonParent is IScope); AstRoot subTree = RParser.Parse(newSnapshot, commonParent); return; } AstRoot newTree = RParser.Parse(newSnapshot); treeChanges.ChangeQueue.Enqueue(new EditorTreeChange_NewTree(newTree)); }
/// <summary> /// Main asyncronous task body /// </summary> void ProcessTextChanges(TextChange changeToProcess, bool async, Func <bool> isCancelledCallback) { lock (_disposeLock) { if (_editorTree == null || _disposed || isCancelledCallback()) { return; } EditorTreeChangeCollection treeChanges = null; // Cache id since it can change if task is canceled long taskId = TaskId; try { AstRoot rootNode; // We only need read lock since changes will be applied // from the main thread if (async) { rootNode = _editorTree.AcquireReadLock(_treeUserId); } else { rootNode = _editorTree.GetAstRootUnsafe(); } treeChanges = new EditorTreeChangeCollection(changeToProcess.Version, changeToProcess.FullParseRequired); TextChangeProcessor changeProcessor = new TextChangeProcessor(_editorTree, rootNode, isCancelledCallback); bool fullParseRequired = changeToProcess.FullParseRequired; if (fullParseRequired) { changeProcessor.FullParse(treeChanges, changeToProcess.NewTextProvider); } else { changeProcessor.ProcessChange(changeToProcess, treeChanges); } } finally { if (async && _editorTree != null) { _editorTree.ReleaseReadLock(_treeUserId); } } // Lock should be released at this point since actual application // of tree changes is going to be happen from the main thread. if (!isCancelledCallback() && treeChanges != null) { // Queue results for the main thread application. This must be done before // signaling that the task is complete since if EnsureProcessingComplete // is waiting it will want to apply changes itself rather than wait for // the DispatchOnUIThread to go though and hence it will need all changes // stored and ready for application. _backgroundParsingResults.Enqueue(treeChanges); } // Signal task complete now so if main thread is waiting // it can proceed and appy the changes immediately. SignalTaskComplete(taskId); if (_backgroundParsingResults.Count > 0) { _uiThreadTransitionRequestTime = DateTime.UtcNow; // It is OK to post results while main thread might be working // on them since if if it does, by the time posted request comes // queue will already be empty. if (async) { // Post request to apply tree changes to the main thread. // This must NOT block or else task will never enter 'RanToCompletion' state. EditorShell.DispatchOnUIThread(() => ApplyBackgroundProcessingResults()); } else { // When processing is synchronous, apply changes and fire events right away. ApplyBackgroundProcessingResults(); } } } }