void HandleTextReplacing(object sender, Core.Text.TextChangeEventArgs e) { var handler = TextChanged; if (handler != null) { lock (replaceLock) { var oldText = CurrentText; var changes = new Microsoft.CodeAnalysis.Text.TextChange[e.TextChanges.Count]; var changeRanges = new TextChangeRange[e.TextChanges.Count]; for (int i = 0; i < e.TextChanges.Count; ++i) { var c = e.TextChanges[i]; var span = new TextSpan(c.Offset, c.RemovalLength); changes[i] = new Microsoft.CodeAnalysis.Text.TextChange(span, c.InsertedText.Text); changeRanges[i] = new TextChangeRange(span, c.InsertionLength); } var newText = oldText.WithChanges(changes); currentText = newText; try { handler(this, new Microsoft.CodeAnalysis.Text.TextChangeEventArgs(oldText, newText, changeRanges)); } catch (ArgumentException ae) { LoggingService.LogWarning(ae.Message + " re opening " + editor.FileName + " as roslyn source text."); workspace.InformDocumentClose(Id, editor.FileName); Dispose(); // 100% ensure that this object is disposed if (workspace.GetDocument(Id) != null) { TypeSystemService.InformDocumentOpen(Id, editor); } } catch (Exception ex) { LoggingService.LogError("Error while text replacing", ex); } } } }
void HandleTextReplacing(object sender, Core.Text.TextChangeEventArgs e) { var handler = TextChanged; if (handler != null) { lock (replaceLock) { var oldText = CurrentText; var changes = new Microsoft.CodeAnalysis.Text.TextChange[e.TextChanges.Count]; var changeRanges = new TextChangeRange[e.TextChanges.Count]; for (int i = 0; i < e.TextChanges.Count; ++i) { var c = e.TextChanges[i]; var span = new TextSpan(c.Offset, c.RemovalLength); changes[i] = new Microsoft.CodeAnalysis.Text.TextChange(span, c.InsertedText.Text); changeRanges[i] = new TextChangeRange(span, c.InsertionLength); } var newText = oldText.WithChanges(changes); currentText = newText; try { handler(this, new Microsoft.CodeAnalysis.Text.TextChangeEventArgs(oldText, newText, changeRanges)); } catch (Exception ex) { LoggingService.LogError("Error while text replacing", ex); } } } }
public void IncrementalParsingIsSameAsFullParsing_LeadingTriviaEdgeCase() { /* The formatting here is important, * we are trying to force a situation * where the spacing trivia for the leading * indentation is stored on the "ios" prefix * instead of the attribute name node we are * incrementally parsing */ const string Full = @"<Node ios:otherAttr=""foobar"" android:attribute ios:alpha=""1"" />"; const string IncrementalBase = @"<Node ios:otherAttr=""foobar"" a ios:alpha=""1"" />"; var full = Parser.ParseText(Full); var incremental = Parser.ParseText(IncrementalBase); var additionalText = "ndroid:attribute"; // Complete the attribute one character at a time incrementally for (int i = 1; i <= additionalText.Length; i++) { var insertionIndex = IncrementalBase.IndexOf(" a"); var newIncrementalText = IncrementalBase.Replace(" a", " a" + additionalText.Substring(0, i)); var change = new TextChangeRange(new TextSpan(insertionIndex + i + 1, 0), 1); incremental = Parser.ParseIncremental(new StringBuffer(newIncrementalText), new[] { change }, incremental); } Assert.Equal(full.ToFullString(), incremental.ToFullString()); AssertSameNodes(full, incremental); }
private void RecordChange(TextChangeRange textChangeRange, SyntaxNodeOrToken removedNode, SyntaxNodeOrToken insertedNode) { if (_changes.Count > 0) { var last = _changes[_changes.Count - 1]; if (last.Range.Span.End == textChangeRange.Span.Start) { // merge changes... last.OldNodes?.Enqueue(removedNode); last.NewNodes?.Enqueue(insertedNode); _changes[_changes.Count - 1] = new ChangeRecord( new TextChangeRange(new TextSpan(last.Range.Span.Start, last.Range.Span.Length + textChangeRange.Span.Length), last.Range.NewLength + textChangeRange.NewLength), last.OldNodes ?? CreateQueue(removedNode), last.NewNodes ?? CreateQueue(insertedNode)); return; } Debug.Assert(textChangeRange.Span.Start >= last.Range.Span.End); } _changes.Add(new ChangeRecord(textChangeRange, CreateQueue(removedNode), CreateQueue(insertedNode))); // Local Functions Queue <SyntaxNodeOrToken> CreateQueue(SyntaxNodeOrToken nodeOrToken) { var queue = new Queue <SyntaxNodeOrToken>(); queue.Enqueue(nodeOrToken); return(queue); } }
private void AccumulateTextChanges(TextContentChangedEventArgs contentChanged) { _workQueue.AssertIsForeground(); var contentChanges = contentChanged.Changes; var count = contentChanges.Count; switch (count) { case 0: return; case 1: // PERF: Optimize for the simple case of typing on a line. { var c = contentChanges[0]; var textChangeRange = new TextChangeRange(new TextSpan(c.OldSpan.Start, c.OldSpan.Length), c.NewLength); this.AccumulatedTextChanges = this.AccumulatedTextChanges == null ? textChangeRange : this.AccumulatedTextChanges.Accumulate(SpecializedCollections.SingletonEnumerable(textChangeRange)); } break; default: var textChangeRanges = new TextChangeRange[count]; for (int i = 0; i < count; i++) { var c = contentChanges[i]; textChangeRanges[i] = new TextChangeRange(new TextSpan(c.OldSpan.Start, c.OldSpan.Length), c.NewLength); } this.AccumulatedTextChanges = this.AccumulatedTextChanges.Accumulate(textChangeRanges); break; } }
public void Ctor2() { var span = new TextSpan(2, 50); var range = new TextChangeRange(span, 42); Assert.Equal(span, range.Span); Assert.Equal(42, range.NewLength); }
public void Ctor1() { Assert.Throws <ArgumentOutOfRangeException>( () => { var notUsed = new TextChangeRange(new TextSpan(), -1); } ); }
private void DocumentOnChanged(object sender, DocumentChangeEventArgs e) { SetCurrent(); var textChangeRange = new TextChangeRange( new TextSpan(e.Offset, e.RemovalLength), e.RemovalLength == 0 ? e.InsertionLength : e.RemovalLength); OnTextChanged(new TextChangeEventArgs(_oldText, CurrentText, textChangeRange)); }
private void DocumentOnChanged(object sender, DocumentChangeEventArgs e) { SetCurrent(); var textChangeRange = new TextChangeRange( new TextSpan(e.Offset, e.RemovalLength), e.RemovalLength == 0 ? e.InsertionLength : e.RemovalLength); OnTextChanged(new TextChangeEventArgs(_before, CurrentText, textChangeRange)); }
internal static TextChangeRange[] ToTextChangeRange(this INormalizedTextChangeCollection changes) { var res = new TextChangeRange[changes.Count]; for (int i = 0; i < res.Length; i++) { res[i] = changes[i].ToTextChangeRange(); } return(res); }
private List <ChangeRangeWithText> ReduceChanges(List <ChangeRecord> changeRecords) { var textChanges = new List <ChangeRangeWithText>(changeRecords.Count); var oldText = new StringBuilder(); var newText = new StringBuilder(); foreach (var cr in changeRecords) { // try to reduce change range by finding common characters if (cr.Range.Span.Length > 0 && cr.Range.NewLength > 0) { var range = cr.Range; CopyText(cr.OldNodes, oldText); CopyText(cr.NewNodes, newText); int commonLeadingCount; int commonTrailingCount; GetCommonEdgeLengths(oldText, newText, out commonLeadingCount, out commonTrailingCount); // did we have any common leading or trailing characters between the strings? if (commonLeadingCount > 0 || commonTrailingCount > 0) { range = new TextChangeRange( new TextSpan(range.Span.Start + commonLeadingCount, range.Span.Length - (commonLeadingCount + commonTrailingCount)), range.NewLength - (commonLeadingCount + commonTrailingCount)); if (commonTrailingCount > 0) { newText.Remove(newText.Length - commonTrailingCount, commonTrailingCount); } if (commonLeadingCount > 0) { newText.Remove(0, commonLeadingCount); } } // only include adjusted change if there is still a change if (range.Span.Length > 0 || range.NewLength > 0) { textChanges.Add(new ChangeRangeWithText(range, _computeNewText ? newText.ToString() : null)); } } else { // pure inserts and deletes textChanges.Add(new ChangeRangeWithText(cr.Range, _computeNewText ? GetText(cr.NewNodes) : null)); } } return(textChanges); }
private static SyntaxNode?GetBestGuessChangedMember( ISyntaxFactsService syntaxFactsService, SyntaxNode oldRoot, SyntaxNode newRoot, TextChangeRange range ) { // if either old or new tree contains skipped text, re-analyze whole document if (oldRoot.ContainsSkippedText || newRoot.ContainsSkippedText) { return(null); } // there was top level changes, so we can't use equivalent to see whether two members are same. // so, we use some simple text based heuristic to find a member that has changed. // // if we have a differ that do diff on member level or a way to track member between incremental parsing, then // that would be preferable. but currently we don't have such thing. // get top level elements at the position where change has happened var oldMember = syntaxFactsService.GetContainingMemberDeclaration( oldRoot, range.Span.Start ); var newMember = syntaxFactsService.GetContainingMemberDeclaration( newRoot, range.Span.Start ); // reached the top (compilation unit) if (oldMember == null || newMember == null) { return(null); } // if old member was empty, just use new member if (oldMember.Span.IsEmpty) { return(newMember); } // looks like change doesn't belong to existing member if (!oldMember.Span.Contains(range.Span)) { return(null); } // change happened inside of the old member, check whether new member seems just delta of that change var lengthDelta = range.NewLength - range.Span.Length; return((oldMember.Span.Length + lengthDelta) == newMember.Span.Length ? newMember : null); }
private void DocumentOnChanged(object sender, DocumentChangeEventArgs e) { var oldSourceText = this.currentSourceText; var textSpan = new TextSpan(e.Offset, e.RemovalLength); var textChangeRange = new TextChangeRange(textSpan, e.InsertionLength); this.currentSourceText = this.currentSourceText.WithChanges(new TextChange(textSpan, e.InsertedText?.Text ?? string.Empty)); this.TextChanged?.Invoke(this, new TextChangeEventArgs(oldSourceText, this.currentSourceText, textChangeRange)); }
private static TextChangeRange ExtendToAffectedRange( CSharp.CSharpSyntaxNode oldTree, TextChangeRange changeRange ) { // we will increase affected range of the change by the number of lookahead tokens // original code in Blender seem to imply the lookahead at the end of a node is 1 token // max. TODO: 1 token lookahead seems a bit too optimistic. Increase if needed. const int maxLookahead = 1; // check if change is not after the end. TODO: there should be an assert somewhere about // changes starting at least at the End of old tree var lastCharIndex = oldTree.FullWidth - 1; // Move the start of the change range so that it is contained within oldTree. var start = Math.Max(Math.Min(changeRange.Span.Start, lastCharIndex), 0); // the first iteration aligns us with the change start. subsequent iteration move us to // the left by maxLookahead tokens. We only need to do this as long as we're not at the // start of the tree. Also, the tokens we get back may be zero width. In that case we // need to keep on looking backward. for (var i = 0; start > 0 && i <= maxLookahead;) { var token = oldTree.FindToken(start, findInsideTrivia: false); Debug.Assert( token.Kind() != SyntaxKind.None, "how could we not get a real token back?" ); start = Math.Max(0, token.Position - 1); // Only increment i if we got a non-zero width token. Otherwise, we want to just do // this again having moved back one space. if (token.FullWidth > 0) { i++; } } if (IsInsideInterpolation(oldTree, start)) { // If the changed range starts inside an interpolated string, we // move the start of the change range to the beginning of the line so that any // interpolated string literal in the changed range will be scanned in its entirety. var column = oldTree.SyntaxTree.GetLineSpan(new TextSpan(start, 0)).Span.Start.Character; start = Math.Max(start - column, 0); } var finalSpan = TextSpan.FromBounds(start, changeRange.Span.End); var finalLength = changeRange.NewLength + (changeRange.Span.Start - start); return(new TextChangeRange(finalSpan, finalLength)); }
private void DocumentOnChanged(object sender, DocumentChangeEventArgs e) { if (_updatding) return; var oldText = _currentText; var textSpan = new TextSpan(e.Offset, e.RemovalLength); var textChangeRange = new TextChangeRange(textSpan, e.InsertionLength); _currentText = _currentText.WithChanges(new TextChange(textSpan, e.InsertedText?.Text ?? string.Empty)); TextChanged?.Invoke(this, new TextChangeEventArgs(oldText, _currentText, textChangeRange)); }
public Blender( Lexer lexer, CSharp.CSharpSyntaxNode oldTree, IEnumerable <TextChangeRange> changes ) { Debug.Assert(lexer != null); _lexer = lexer; _changes = ImmutableStack.Create <TextChangeRange>(); if (changes != null) { // TODO: Consider implementing NormalizedChangeCollection for TextSpan. the real // reason why we are collapsing is because we want to extend change ranges and // cannot allow them to overlap. This does not seem to be a big deal since multiple // changes are infrequent and typically close to each other. However if we have // NormalizedChangeCollection for TextSpan we can have both - we can extend ranges // and not require collapsing them. NormalizedChangeCollection would also ensure // that changes are always normalized. // TODO: this is a temporary measure to prevent individual change spans from // overlapping after they are widened to effective width (+1 token at the start). // once we have normalized collection for TextSpan we will not need to collapse all // the change spans. var collapsed = TextChangeRange.Collapse(changes); // extend the change to its affected range. This will make it easier // to filter out affected nodes since we will be able simply check // if node intersects with a change. var affectedRange = ExtendToAffectedRange(oldTree, collapsed); _changes = _changes.Push(affectedRange); } if (oldTree == null) { // start at lexer current position if no nodes specified _oldTreeCursor = new Cursor(); _newPosition = lexer.TextWindow.Position; } else { _oldTreeCursor = Cursor.FromRoot(oldTree).MoveToFirstChild(); _newPosition = 0; } _changeDelta = 0; _newDirectives = default(DirectiveStack); _oldDirectives = default(DirectiveStack); _newLexerDrivenMode = 0; }
private void EhAvalonEditsDocumentChanged(object sender, DocumentChangeEventArgs e) { if (_updatingCount > 0) { return; } var oldText = _currentSourceText_Roslyn; var textSpan = new TextSpan(e.Offset, e.RemovalLength); var textChangeRange = new TextChangeRange(textSpan, e.InsertionLength); _currentSourceText_Roslyn = _currentSourceText_Roslyn.WithChanges(new TextChange(textSpan, e.InsertedText?.Text ?? string.Empty)); TextChanged?.Invoke(this, new TextChangeEventArgs(oldText, _currentSourceText_Roslyn, textChangeRange)); }
private void DocumentOnChanged(object sender, DocumentChangeEventArgs e) { if (_updatding) { return; } var oldText = _currentText; var textSpan = new TextSpan(e.Offset, e.RemovalLength); var textChangeRange = new TextChangeRange(textSpan, e.InsertionLength); _currentText = _currentText.WithChanges(new TextChange(textSpan, e.InsertedText?.Text ?? string.Empty)); TextChanged?.Invoke(this, new TextChangeEventArgs(oldText, _currentText, textChangeRange)); }
void T(string originalText, string replacementText) { var offset = Xml.IndexOf(originalText); var length = originalText.Length; var newLength = replacementText.Length; var newXml = Xml.Replace(originalText, replacementText); var changes = new TextChangeRange[] { new TextChangeRange(new TextSpan(offset, length), newLength) }; var root = Parser.ParseText(Xml); var newRoot = Parser.ParseIncremental(newXml, changes, root); Assert.NotSame(root, newRoot); Assert.Equal(newXml, newRoot.ToFullString()); AssertShareElementGreenNodesWithName("Y", 2, root.Body, newRoot.Body); AssertShareAttributeGreenNodesWithPrefix("reused", 2, root.Body, newRoot.Body); }
private void AccumulateTextChanges(TextContentChangedEventArgs contentChanged) { var contentChanges = contentChanged.Changes; var count = contentChanges.Count; switch (count) { case 0: return; case 1: // PERF: Optimize for the simple case of typing on a line. { var c = contentChanges[0]; var textChangeRange = new TextChangeRange(new TextSpan(c.OldSpan.Start, c.OldSpan.Length), c.NewLength); lock (_cachedTagsGate) { if (_accumulatedTextChanges == null) { _accumulatedTextChanges = textChangeRange; } else { _accumulatedTextChanges = _accumulatedTextChanges.Accumulate(SpecializedCollections.SingletonEnumerable(textChangeRange)); } } } break; default: var textChangeRanges = new TextChangeRange[count]; for (int i = 0; i < count; i++) { var c = contentChanges[i]; textChangeRanges[i] = new TextChangeRange(new TextSpan(c.OldSpan.Start, c.OldSpan.Length), c.NewLength); } lock (_cachedTagsGate) { _accumulatedTextChanges = _accumulatedTextChanges.Accumulate(textChangeRanges); } break; } }
private static IEnumerable <ClassifiedSpan> GetChangedClassifiedSpans(TextChangeRange textChange, IEnumerable <ClassifiedSpan> oldClassifiedSpans, IEnumerable <ClassifiedSpan> newClassifiedSpans) { int offset = textChange.NewLength - textChange.Span.Length; foreach (ClassifiedSpan span in newClassifiedSpans) { ClassifiedSpan offsettedSpan = span; if (span.TextSpan.Start > textChange.Span.End && span.TextSpan.Start > offset) { offsettedSpan = new ClassifiedSpan(span.ClassificationType, new TextSpan(span.TextSpan.Start - offset, span.TextSpan.Length)); } if (!oldClassifiedSpans.Contains(offsettedSpan)) { yield return(span); } } }
private static SyntaxNode?GetChangedMember( ISyntaxFactsService syntaxFactsService, SyntaxNode oldRoot, SyntaxNode newRoot, TextChangeRange range ) { // if either old or new tree contains skipped text, re-analyze whole document if (oldRoot.ContainsSkippedText || newRoot.ContainsSkippedText) { return(null); } var oldMember = syntaxFactsService.GetContainingMemberDeclaration( oldRoot, range.Span.Start ); var newMember = syntaxFactsService.GetContainingMemberDeclaration( newRoot, range.Span.Start ); // reached the top (compilation unit) if (oldMember == null || newMember == null) { return(null); } // member doesn't contain the change if (!syntaxFactsService.ContainsInMemberBody(oldMember, range.Span)) { return(null); } // member signature has changed if (!oldMember.IsEquivalentTo(newMember, topLevel: true)) { return(null); } // looks like inside of the body has changed return(newMember); }
private void AccumulateTextChanges(TextContentChangedEventArgs contentChanged) { this.AssertIsForeground(); var contentChanges = contentChanged.Changes; var count = contentChanges.Count; switch (count) { case 0: return; case 1: // PERF: Optimize for the simple case of typing on a line. { var c = contentChanges[0]; var textChangeRange = new TextChangeRange(new TextSpan(c.OldSpan.Start, c.OldSpan.Length), c.NewLength); this.AccumulatedTextChanges = this.AccumulatedTextChanges == null ? textChangeRange : this.AccumulatedTextChanges.Accumulate(SpecializedCollections.SingletonEnumerable(textChangeRange)); } break; default: { using var _ = ArrayBuilder <TextChangeRange> .GetInstance(count, out var textChangeRanges); foreach (var c in contentChanges) { textChangeRanges.Add(new TextChangeRange(new TextSpan(c.OldSpan.Start, c.OldSpan.Length), c.NewLength)); } this.AccumulatedTextChanges = this.AccumulatedTextChanges.Accumulate(textChangeRanges); break; } } }
public void Ctor1() { Assert.Throws( typeof(ArgumentOutOfRangeException), () => { var notUsed = new TextChangeRange(new TextSpan(), -1); }); }
internal ChangeRecord(TextChangeRange range, Stack<SyntaxNodeOrToken> oldNodes, Stack<SyntaxNodeOrToken> newNodes) { this.Range = range; this.OldNodes = oldNodes; this.NewNodes = newNodes; }
/// <summary> /// Merges the new change ranges into the old change ranges, adjusting the new ranges to be with respect to the original text /// (with neither old or new changes applied) instead of with respect to the original text after "old changes" are applied. /// /// This may require splitting, concatenation, etc. of individual change ranges. /// </summary> /// <remarks> /// Both `oldChanges` and `newChanges` must contain non-overlapping spans in ascending order. /// </remarks> public static ImmutableArray <TextChangeRange> Merge(ImmutableArray <TextChangeRange> oldChanges, ImmutableArray <TextChangeRange> newChanges) { // Earlier steps are expected to prevent us from ever reaching this point with empty change sets. if (oldChanges.IsEmpty) { throw new ArgumentException(nameof(oldChanges)); } if (newChanges.IsEmpty) { throw new ArgumentException(nameof(newChanges)); } var builder = ArrayBuilder <TextChangeRange> .GetInstance(); var oldChange = oldChanges[0]; var newChange = new UnadjustedNewChange(newChanges[0]); var oldIndex = 0; var newIndex = 0; // The sum of characters inserted by old changes minus characters deleted by old changes. // This value must be adjusted whenever characters from an old change are added to `builder`. var oldDelta = 0; // In this loop we "zip" together potentially overlapping old and new changes. // It's important that when overlapping changes are found, we don't consume past the end of the overlapping section until the next iteration. // so that we don't miss scenarios where the section after the overlap we found itself overlaps with another change // e.g.: // [-------oldChange1------] // [--newChange1--] [--newChange2--] while (true) { if (oldChange.Span.Length == 0 && oldChange.NewLength == 0) { // old change does not insert or delete any characters, so it can be dropped to no effect. if (tryGetNextOldChange()) { continue; } else { break; } } else if (newChange.SpanLength == 0 && newChange.NewLength == 0) { // new change does not insert or delete any characters, so it can be dropped to no effect. if (tryGetNextNewChange()) { continue; } else { break; } } else if (newChange.SpanEnd <= oldChange.Span.Start + oldDelta) { // new change is entirely before old change, so just take the new change // old[--------] // new[--------] adjustAndAddNewChange(builder, oldDelta, newChange); if (tryGetNextNewChange()) { continue; } else { break; } } else if (newChange.SpanStart >= oldChange.NewEnd() + oldDelta) { // new change is entirely after old change, so just take the old change // old[--------] // new[--------] addAndAdjustOldDelta(builder, ref oldDelta, oldChange); if (tryGetNextOldChange()) { continue; } else { break; } } else if (newChange.SpanStart < oldChange.Span.Start + oldDelta) { // new change starts before old change, but the new change deletion overlaps with the old change insertion // note: 'd' represents a deleted character, 'a' represents a character inserted by an old change, and 'b' represents a character inserted by a new change. // // old|dddddd| // |aaaaaa| // --------------- // new|dddddd| // |bbbbbb| // align the new change and old change start by consuming the part of the new deletion before the old change // (this only deletes characters of the original text) // // old|dddddd| // |aaaaaa| // --------------- // new|ddd| // |bbbbbb| var newChangeLeadingDeletion = oldChange.Span.Start + oldDelta - newChange.SpanStart; adjustAndAddNewChange(builder, oldDelta, new UnadjustedNewChange(newChange.SpanStart, newChangeLeadingDeletion, newLength: 0)); newChange = new UnadjustedNewChange(oldChange.Span.Start + oldDelta, newChange.SpanLength - newChangeLeadingDeletion, newChange.NewLength); continue; } else if (newChange.SpanStart > oldChange.Span.Start + oldDelta) { // new change starts after old change, but overlaps // // old|dddddd| // |aaaaaa| // --------------- // new|dddddd| // |bbbbbb| // align the old change to the new change by consuming the part of the old change which is before the new change. // // old|ddd| // |aaa| // --------------- // new|dddddd| // |bbbbbb| var oldChangeLeadingInsertion = newChange.SpanStart - (oldChange.Span.Start + oldDelta); // we must make sure to delete at most as many characters as the entire oldChange deletes var oldChangeLeadingDeletion = Math.Min(oldChange.Span.Length, oldChangeLeadingInsertion); addAndAdjustOldDelta(builder, ref oldDelta, new TextChangeRange(new TextSpan(oldChange.Span.Start, oldChangeLeadingDeletion), oldChangeLeadingInsertion)); oldChange = new TextChangeRange(new TextSpan(newChange.SpanStart - oldDelta, oldChange.Span.Length - oldChangeLeadingDeletion), oldChange.NewLength - oldChangeLeadingInsertion); continue; } else { // old and new change start at same adjusted position Debug.Assert(newChange.SpanStart == oldChange.Span.Start + oldDelta); if (newChange.SpanLength <= oldChange.NewLength) { // new change deletes fewer characters than old change inserted // // old|dddddd| // |aaaaaa| // --------------- // new|ddd| // |bbbbbb| // - apply the new change deletion to the old change insertion // // old|dddddd| // |aaa| // --------------- // new|| // |bbbbbb| // // - move the new change insertion forward by the same amount as its consumed deletion to remain aligned with the old change. // (because the old change and new change have the same adjusted start position, the new change insertion appears directly before the old change insertion in the final text) // // old|dddddd| // |aaa| // --------------- // new|| // |bbbbbb| oldChange = new TextChangeRange(oldChange.Span, oldChange.NewLength - newChange.SpanLength); // the new change deletion is equal to the subset of the old change insertion that we are consuming this iteration oldDelta = oldDelta + newChange.SpanLength; // since the new change insertion occurs before the old change, consume it now newChange = new UnadjustedNewChange(newChange.SpanEnd, spanLength: 0, newChange.NewLength); adjustAndAddNewChange(builder, oldDelta, newChange); if (tryGetNextNewChange()) { continue; } else { break; } } else { // new change deletes more characters than old change inserted // // old|d| // |aa| // --------------- // new|ddd| // |bbb| // merge the old change into the new change: // - new change deletion deletes all of the old change insertion. reduce the new change deletion accordingly // // old|d| // || // --------------- // new|d| // |bbb| // // - old change deletion is simply added to the new change deletion. // // old|| // || // --------------- // new|dd| // |bbb| // // - new change is moved to put its adjusted position equal to the old change we just merged in // // old|| // || // --------------- // new|dd| // |bbb| // adjust the oldDelta to reflect that the old change has been consumed oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength; var newDeletion = newChange.SpanLength + oldChange.Span.Length - oldChange.NewLength; newChange = new UnadjustedNewChange(oldChange.Span.Start + oldDelta, newDeletion, newChange.NewLength); if (tryGetNextOldChange()) { continue; } else { break; } } } } // there may be remaining old changes or remaining new changes (not both, and not neither) switch (oldIndex == oldChanges.Length, newIndex == newChanges.Length) {
internal static void VerifySource(this SyntaxTree tree, IEnumerable <TextChangeRange> changes = null) { var root = tree.GetRoot(); var text = tree.GetText(); var fullSpan = new TextSpan(0, text.Length); SyntaxNode node = null; // If only a subset of the document has changed, // just check that subset to reduce verification cost. if (changes != null) { var change = TextChangeRange.Collapse(changes).Span; if (change != fullSpan) { // Find the lowest node in the tree that contains the changed region. node = root.DescendantNodes(n => n.FullSpan.Contains(change)).LastOrDefault(); } } if (node == null) { node = root; } var span = node.FullSpan; var textSpanOpt = span.Intersection(fullSpan); int index; if (textSpanOpt == null) { index = 0; } else { var fromText = text.ToString(textSpanOpt.Value); var fromNode = node.ToFullString(); index = FindFirstDifference(fromText, fromNode); } if (index >= 0) { index += span.Start; string message; if (index < text.Length) { var position = text.Lines.GetLinePosition(index); var line = text.Lines[position.Line]; var allText = text.ToString(); // Entire document as string to allow inspecting the text in the debugger. message = string.Format("Unexpected difference at offset {0}: Line {1}, Column {2} \"{3}\"", index, position.Line + 1, position.Character + 1, line.ToString()); } else { message = "Unexpected difference past end of the file"; } Debug.Assert(false, message); } }
private List<ChangeRangeWithText> ReduceChanges(List<ChangeRecord> changeRecords) { var textChanges = new List<ChangeRangeWithText>(changeRecords.Count); var oldText = new StringBuilder(); var newText = new StringBuilder(); foreach (var cr in changeRecords) { // try to reduce change range by finding common characters if (cr.Range.Span.Length > 0 && cr.Range.NewLength > 0) { var range = cr.Range; CopyText(cr.OldNodes, oldText); CopyText(cr.NewNodes, newText); int commonLeadingCount; int commonTrailingCount; GetCommonEdgeLengths(oldText, newText, out commonLeadingCount, out commonTrailingCount); // did we have any common leading or trailing characters between the strings? if (commonLeadingCount > 0 || commonTrailingCount > 0) { range = new TextChangeRange( new TextSpan(range.Span.Start + commonLeadingCount, range.Span.Length - (commonLeadingCount + commonTrailingCount)), range.NewLength - (commonLeadingCount + commonTrailingCount)); if (commonTrailingCount > 0) { newText.Remove(newText.Length - commonTrailingCount, commonTrailingCount); } if (commonLeadingCount > 0) { newText.Remove(0, commonLeadingCount); } } // only include adjusted change if there is still a change if (range.Span.Length > 0 || range.NewLength > 0) { textChanges.Add(new ChangeRangeWithText(range, _computeNewText ? newText.ToString() : null)); } } else { // pure inserts and deletes textChanges.Add(new ChangeRangeWithText(cr.Range, _computeNewText ? GetText(cr.NewNodes) : null)); } } return textChanges; }
private static void AddRange(List<TextChangeRange> list, TextChangeRange range) { if (list.Count > 0) { var last = list[list.Count - 1]; if (last.Span.End == range.Span.Start) { // merge changes together if they are adjacent list[list.Count - 1] = new TextChangeRange(new TextSpan(last.Span.Start, last.Span.Length + range.Span.Length), last.NewLength + range.NewLength); return; } else { Debug.Assert(range.Span.Start > last.Span.End); } } list.Add(range); }
internal ChangeRecord(TextChangeRange range, List <SyntaxNodeOrToken> newNodes) { this.Range = range; this.NewNodes = newNodes; }
private static SyntaxNode GetChangedMember( ISyntaxFactsService syntaxFactsService, SyntaxNode oldRoot, SyntaxNode newRoot, TextChangeRange range) { // if either old or new tree contains skipped text, re-analyze whole document if (oldRoot.ContainsSkippedText || newRoot.ContainsSkippedText) { return null; } var oldMember = syntaxFactsService.GetContainingMemberDeclaration(oldRoot, range.Span.Start); var newMember = syntaxFactsService.GetContainingMemberDeclaration(newRoot, range.Span.Start); // reached the top (compilation unit) if (oldMember == null || newMember == null) { return null; } // member doesn't contain the change if (!syntaxFactsService.ContainsInMemberBody(oldMember, range.Span)) { return null; } // member signature has changed if (!oldMember.IsEquivalentTo(newMember, topLevel: true)) { return null; } // looks like inside of the body has changed return newMember; }
private static SyntaxNode GetBestGuessChangedMember( ISyntaxFactsService syntaxFactsService, SyntaxNode oldRoot, SyntaxNode newRoot, TextChangeRange range) { // if either old or new tree contains skipped text, re-analyze whole document if (oldRoot.ContainsSkippedText || newRoot.ContainsSkippedText) { return null; } // there was top level changes, so we can't use equivalent to see whether two members are same. // so, we use some simple text based heuristic to find a member that has changed. // // if we have a differ that do diff on member level or a way to track member between incremental parsing, then // that would be preferable. but currently we don't have such thing. // get top level elements at the position where change has happened var oldMember = syntaxFactsService.GetContainingMemberDeclaration(oldRoot, range.Span.Start); var newMember = syntaxFactsService.GetContainingMemberDeclaration(newRoot, range.Span.Start); // reached the top (compilation unit) if (oldMember == null || newMember == null) { return null; } // if old member was empty, just use new member if (oldMember.Span.IsEmpty) { return newMember; } // looks like change doesn't belong to existing member if (!oldMember.Span.Contains(range.Span)) { return null; } // change happened inside of the old member, check whether new member seems just delta of that change var lengthDelta = range.NewLength - range.Span.Length; return (oldMember.Span.Length + lengthDelta) == newMember.Span.Length ? newMember : null; }
public ChangeRangeWithText(TextChangeRange range, string newText) { this.Range = range; this.NewText = newText; }
private static ImmutableArray<TextChangeRange> Merge(ImmutableArray<TextChangeRange> oldChanges, ImmutableArray<TextChangeRange> newChanges) { var list = new List<TextChangeRange>(oldChanges.Length + newChanges.Length); int oldIndex = 0; int newIndex = 0; int oldDelta = 0; nextNewChange: if (newIndex < newChanges.Length) { var newChange = newChanges[newIndex]; nextOldChange: if (oldIndex < oldChanges.Length) { var oldChange = oldChanges[oldIndex]; tryAgain: if (oldChange.Span.Length == 0 && oldChange.NewLength == 0) { // old change is a non-change, just ignore it and move on oldIndex++; goto nextOldChange; } else if (newChange.Span.Length == 0 && newChange.NewLength == 0) { // new change is a non-change, just ignore it and move on newIndex++; goto nextNewChange; } else if (newChange.Span.End < (oldChange.Span.Start + oldDelta)) { // new change occurs entirely before old change var adjustedNewChange = new TextChangeRange(new TextSpan(newChange.Span.Start - oldDelta, newChange.Span.Length), newChange.NewLength); AddRange(list, adjustedNewChange); newIndex++; goto nextNewChange; } else if (newChange.Span.Start > oldChange.Span.Start + oldDelta + oldChange.NewLength) { // new change occurs entirely after old change AddRange(list, oldChange); oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength; oldIndex++; goto nextOldChange; } else if (newChange.Span.Start < oldChange.Span.Start + oldDelta) { // new change starts before old change, but overlaps // add as much of new change deletion as possible and try again var newChangeLeadingDeletion = (oldChange.Span.Start + oldDelta) - newChange.Span.Start; AddRange(list, new TextChangeRange(new TextSpan(newChange.Span.Start - oldDelta, newChangeLeadingDeletion), 0)); newChange = new TextChangeRange(new TextSpan(oldChange.Span.Start + oldDelta, newChange.Span.Length - newChangeLeadingDeletion), newChange.NewLength); goto tryAgain; } else if (newChange.Span.Start > oldChange.Span.Start + oldDelta) { // new change starts after old change, but overlaps // add as much of the old change as possible and try again var oldChangeLeadingInsertion = newChange.Span.Start - (oldChange.Span.Start + oldDelta); AddRange(list, new TextChangeRange(oldChange.Span, oldChangeLeadingInsertion)); oldDelta = oldDelta - oldChange.Span.Length + oldChangeLeadingInsertion; oldChange = new TextChangeRange(new TextSpan(oldChange.Span.Start, 0), oldChange.NewLength - oldChangeLeadingInsertion); newChange = new TextChangeRange(new TextSpan(oldChange.Span.Start + oldDelta, newChange.Span.Length), newChange.NewLength); goto tryAgain; } else if (newChange.Span.Start == oldChange.Span.Start + oldDelta) { // new change and old change start at same position if (oldChange.NewLength == 0) { // old change is just a deletion, go ahead and old change now and deal with new change separately AddRange(list, oldChange); oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength; oldIndex++; goto nextOldChange; } else if (newChange.Span.Length == 0) { // new change is just an insertion, go ahead and tack it on with old change AddRange(list, new TextChangeRange(oldChange.Span, oldChange.NewLength + newChange.NewLength)); oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength; oldIndex++; newIndex++; goto nextNewChange; } else { // delete as much from old change as new change can // a new change deletion is a reduction in the old change insertion var oldChangeReduction = Math.Min(oldChange.NewLength, newChange.Span.Length); AddRange(list, new TextChangeRange(oldChange.Span, oldChange.NewLength - oldChangeReduction)); oldDelta = oldDelta - oldChange.Span.Length + (oldChange.NewLength - oldChangeReduction); oldIndex++; // deduct the amount removed from oldChange from newChange's deletion span (since its already been applied) newChange = new TextChangeRange(new TextSpan(oldChange.Span.Start + oldDelta, newChange.Span.Length - oldChangeReduction), newChange.NewLength); goto nextOldChange; } } } else { // no more old changes, just add adjusted new change var adjustedNewChange = new TextChangeRange(new TextSpan(newChange.Span.Start - oldDelta, newChange.Span.Length), newChange.NewLength); AddRange(list, adjustedNewChange); newIndex++; goto nextNewChange; } } else { // no more new changes, just add remaining old changes while (oldIndex < oldChanges.Length) { AddRange(list, oldChanges[oldIndex]); oldIndex++; } } return list.ToImmutableArray(); }
internal ChangeRecord(TextChangeRange range, List<SyntaxNodeOrToken> newNodes) { this.Range = range; this.NewNodes = newNodes; }
internal ChangeRecord(TextChangeRange range, Queue <SyntaxNodeOrToken>?oldNodes, Queue <SyntaxNodeOrToken>?newNodes) { Range = range; OldNodes = oldNodes; NewNodes = newNodes; }
public ChangeRangeWithText(TextChangeRange range, string?newText) { Range = range; NewText = newText; }
/// <summary> /// Affected range of a change is the range within which nodes can be affected by a change /// and cannot be reused. Because of lookahead effective range of a change is larger than /// the change itself. /// </summary> private static TextChangeRange ExtendToAffectedRange( CSharp.CSharpSyntaxNode oldTree, TextChangeRange changeRange) { // we will increase affected range of the change by the number of lookahead tokens // original code in Blender seem to imply the lookahead at the end of a node is 1 token // max. TODO: 1 token lookahead seems a bit too optimistic. Increase if needed. const int maxLookahead = 1; // check if change is not after the end. TODO: there should be an assert somwhere about // changes starting at least at the End of old tree var lastCharIndex = oldTree.FullWidth - 1; // Move the start of the change range so that it is contained within oldTree. var start = Math.Max(Math.Min(changeRange.Span.Start, lastCharIndex), 0); // the first iteration aligns us with the change start. subsequent iteration move us to // the left by maxLookahead tokens. We only need to do this as long as we're not at the // start of the tree. Also, the tokens we get back may be zero width. In that case we // need to keep on looking backward. for (var i = 0; start > 0 && i <= maxLookahead;) { var token = oldTree.FindToken(start, findInsideTrivia: false); Debug.Assert(token.Kind() != SyntaxKind.None, "how could we not get a real token back?"); start = Math.Max(0, token.Position - 1); // Only increment i if we got a non-zero width token. Otherwise, we want to just do // this again having moved back one space. if (token.FullWidth > 0) { i++; } } if (IsInsideInterpolation(oldTree, start)) { // If the changed range starts inside an interpolated string, we // move the start of the change range to the beginning of the line so that any // interpolated string literal in the changed range will be scanned in its entirety. var column = oldTree.SyntaxTree.GetLineSpan(new TextSpan(start, 0)).Span.Start.Character; start = Math.Max(start - column, 0); } var finalSpan = TextSpan.FromBounds(start, changeRange.Span.End); var finalLength = changeRange.NewLength + (changeRange.Span.Start - start); return new TextChangeRange(finalSpan, finalLength); }
public static TextChangeRange?Accumulate(this TextChangeRange?accumulatedTextChangeSoFar, IEnumerable <TextChangeRange> changesInNextVersion) { if (!changesInNextVersion.Any()) { return(accumulatedTextChangeSoFar); } // get encompassing text change and accumulate it once. // we could apply each one individually like we do in SyntaxDiff::ComputeSpansInNew by calculating delta // between each change in changesInNextVersion which is already sorted in its textual position ascending order. // but end result will be same as just applying it once with encompassed text change range. var newChange = TextChangeRange.Collapse(changesInNextVersion); // no previous accumulated change, return the new value. if (accumulatedTextChangeSoFar == null) { return(newChange); } // set initial value from the old one. var currentStart = accumulatedTextChangeSoFar.Value.Span.Start; var currentOldEnd = accumulatedTextChangeSoFar.Value.Span.End; var currentNewEnd = accumulatedTextChangeSoFar.Value.Span.Start + accumulatedTextChangeSoFar.Value.NewLength; // this is a port from // csharp\rad\Text\SourceText.cpp - CSourceText::OnChangeLineText // which accumulate text changes to one big text change that would encompass all changes // Merge incoming edit data with old edit data here. // RULES: // 1) position values are always associated with a buffer version. // 2) Comparison between position values is only allowed if their // buffer version is the same. // 3) newChange.Span.End and newChange.Span.Start + newChange.NewLength (both stored and incoming) // refer to the same position, but have different buffer versions. // 4) The incoming end position is associated with buffer versions // n-1 (old) and n(new). // 5) The stored end position BEFORE THIS EDIT is associated with // buffer versions 0 (old) and n-1 (new). // 6) The stored end position AFTER THIS EDIT should be associated // with buffer versions 0 (old) and n(new). // 7) To transform a position P from buffer version of x to y, apply // the delta between any position C(x) and C(y), ASSUMING that // both positions P and C are affected by all edits between // buffer versions x and y. // 8) The start position is relative to all buffer versions, because // it precedes all edits(by definition) // First, the start position. This one is easy, because it is not // complicated by buffer versioning -- it is always the "earliest" // of all incoming values. if (newChange.Span.Start < currentStart) { currentStart = newChange.Span.Start; } // Okay, now the end position. We must make a choice between the // stored end position and the incoming end position. Per rule #2, // we must use the stored NEW end and the incoming OLD end, both of // which are relative to buffer version n-1. if (currentNewEnd > newChange.Span.End) { // We have chosen to keep the stored end because it occurs past // the incoming edit. So, we need currentOldEnd and // currentNewEnd. Since currentOldEnd is already relative // to buffer 0, it is unmodified. Since currentNewEnd is // relative to buffer n-1 (and we need n), apply to it the delta // between the incoming end position values, which are n-1 and n. currentNewEnd = currentNewEnd + newChange.NewLength - newChange.Span.Length; } else { // We have chosen to use the incoming end because it occurs past // the stored edit. So, we need newChange.Span.End and (newChange.Span.Start + newChange.NewLength). // Since (newChange.Span.Start + newChange.NewLength) is already relative to buffer n, it is copied // unmodified. Since newChange.Span.End is relative to buffer n-1 (and // we need 0), apply to it the delta between the stored end // position values, which are relative to 0 and n-1. currentOldEnd = currentOldEnd + newChange.Span.End - currentNewEnd; currentNewEnd = newChange.Span.Start + newChange.NewLength; } return(new TextChangeRange(TextSpan.FromBounds(currentStart, currentOldEnd), currentNewEnd - currentStart)); }
internal static TextChangeRange[] ToTextChangeRange(this INormalizedTextChangeCollection changes) { var res = new TextChangeRange[changes.Count]; for (int i = 0; i < res.Length; i++) res[i] = changes[i].ToTextChangeRange(); return res; }
internal ChangeRecord(TextChangeRange range, Queue <SyntaxNodeOrToken> oldNodes, Queue <SyntaxNodeOrToken> newNodes) { this.Range = range; this.OldNodes = oldNodes; this.NewNodes = newNodes; }