| File: Parser\Blender.cs | Web Access |
| Project: src\roslyn\src\Compilers\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.csproj (Microsoft.CodeAnalysis.CSharp) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. #nullable disable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax { internal readonly partial struct Blender { private readonly Lexer _lexer; private readonly Cursor _oldTreeCursor; private readonly ImmutableStack<TextChangeRange> _changes; /// <summary> /// newPosition represents the position we are in the final SourceText. As we consume and reuse /// nodes from the old tree we will update our position in the new text accordingly. /// Likewise, when we must lex tokens out of the new tree we will update as well. /// /// NOTE(cyrusn): We do not need an oldPosition because it is redundant given the /// oldTreeCursor. The oldPosition is implicitly defined by the position of the cursor. /// </summary> private readonly int _newPosition; private readonly int _changeDelta; private readonly DirectiveStack _newDirectives; private readonly DirectiveStack _oldDirectives; private readonly LexerMode _newLexerDrivenMode; 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 Blender( Lexer lexer, Cursor oldTreeCursor, ImmutableStack<TextChangeRange> changes, int newPosition, int changeDelta, DirectiveStack newDirectives, DirectiveStack oldDirectives, LexerMode newLexerDrivenMode) { Debug.Assert(lexer != null); Debug.Assert(changes != null); Debug.Assert(newPosition >= 0); _lexer = lexer; _oldTreeCursor = oldTreeCursor; _changes = changes; _newPosition = newPosition; _changeDelta = changeDelta; _newDirectives = newDirectives; _oldDirectives = oldDirectives; _newLexerDrivenMode = newLexerDrivenMode & (LexerMode.MaskXmlDocCommentLocation | LexerMode.MaskXmlDocCommentStyle); } /// <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 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 static bool IsInsideInterpolation(CSharp.CSharpSyntaxNode oldTree, int start) { var token = oldTree.FindToken(start, findInsideTrivia: false); for (var parent = token.Parent; // for each parent parent != null; parent = parent.Parent) { if (parent.Kind() == SyntaxKind.InterpolatedStringExpression) { return true; } } return false; } public BlendedNode ReadNode(LexerMode mode) { return ReadNodeOrToken(mode, asToken: false); } public BlendedNode ReadToken(LexerMode mode) { return ReadNodeOrToken(mode, asToken: true); } private BlendedNode ReadNodeOrToken(LexerMode mode, bool asToken) { var reader = new Reader(this); return reader.ReadNodeOrToken(mode, asToken); } } }