Introduce SourceChange in place of TextChange

This is the first step on the journey to replacing RazorEditorParser. We
can't just get rid of this because VS is using it today.
This commit is contained in:
Ryan Nowak 2017-05-03 00:49:40 -07:00
parent e6c8ea8341
commit e15d1be616
19 changed files with 545 additions and 240 deletions

View File

@ -31,11 +31,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
public string AutoCompleteString { get; set; }
protected override PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange)
protected override PartialParseResult CanAcceptChange(Span target, SourceChange change)
{
if (((AutoCompleteAtEndOfSpan && IsAtEndOfSpan(target, normalizedChange)) || IsAtEndOfFirstLine(target, normalizedChange)) &&
normalizedChange.IsInsert &&
ParserHelpers.IsNewLine(normalizedChange.NewText) &&
if (((AutoCompleteAtEndOfSpan && IsAtEndOfSpan(target, change)) || IsAtEndOfFirstLine(target, change)) &&
change.IsInsert &&
ParserHelpers.IsNewLine(change.NewText) &&
AutoCompleteString != null)
{
return PartialParseResult.Rejected | PartialParseResult.AutoCompleteBlock;

View File

@ -43,7 +43,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
_main.Cancel();
}
#pragma warning disable CS0612 // Type or member is obsolete
public void QueueChange(TextChange change)
#pragma warning restore CS0612 // Type or member is obsolete
{
_main.QueueChange(change);
}
@ -67,37 +69,43 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
}
internal static bool TreesAreDifferent(RazorSyntaxTree leftTree, RazorSyntaxTree rightTree, IEnumerable<TextChange> changes)
{
return TreesAreDifferent(leftTree, rightTree, changes, CancellationToken.None);
}
#pragma warning disable CS0612 // Type or member is obsolete
internal static bool TreesAreDifferent(RazorSyntaxTree leftTree, RazorSyntaxTree rightTree, IEnumerable<TextChange> changes, CancellationToken cancelToken)
#pragma warning restore CS0612 // Type or member is obsolete
{
return TreesAreDifferent(leftTree.Root, rightTree.Root, changes, cancelToken);
}
#pragma warning disable CS0612 // Type or member is obsolete
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable<TextChange> changes)
#pragma warning restore CS0612 // Type or member is obsolete
{
return TreesAreDifferent(leftTree, rightTree, changes, CancellationToken.None);
}
#pragma warning disable CS0612 // Type or member is obsolete
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable<TextChange> changes, CancellationToken cancelToken)
#pragma warning restore CS0612 // Type or member is obsolete
{
// Apply all the pending changes to the original tree
// PERF: If this becomes a bottleneck, we can probably do it the other way around,
// i.e. visit the tree and find applicable changes for each node.
foreach (TextChange change in changes)
#pragma warning disable CS0612 // Type or member is obsolete
foreach (var change in changes)
#pragma warning restore CS0612 // Type or member is obsolete
{
cancelToken.ThrowIfCancellationRequested();
var changeOwner = leftTree.LocateOwner(change);
var sourceChange = change.AsSourceChange();
var changeOwner = leftTree.LocateOwner(sourceChange);
// Apply the change to the tree
if (changeOwner == null)
{
return true;
}
var result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true);
var result = changeOwner.EditHandler.ApplyChange(changeOwner, sourceChange, force: true);
changeOwner.ReplaceWith(result.EditedSpan);
}
@ -150,7 +158,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
private string _fileName;
private readonly object _stateLock = new object();
#pragma warning disable CS0612 // Type or member is obsolete
private IList<TextChange> _changes = new List<TextChange>();
#pragma warning restore CS0612 // Type or member is obsolete
public MainThreadState(string fileName)
{
@ -189,7 +199,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return new DisposableAction(() => Monitor.Exit(_stateLock));
}
#pragma warning disable CS0612 // Type or member is obsolete
public void QueueChange(TextChange change)
#pragma warning restore CS0612 // Type or member is obsolete
{
EnsureOnThread();
lock (_stateLock)
@ -216,7 +228,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
_currentParcelCancelSource = new CancellationTokenSource();
var changes = _changes;
#pragma warning disable CS0612 // Type or member is obsolete
_changes = new List<TextChange>();
#pragma warning restore CS0612 // Type or member is obsolete
return new WorkParcel(changes, _currentParcelCancelSource.Token);
}
}
@ -274,7 +288,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
private RazorTemplateEngine _templateEngine;
private string _fileName;
private RazorSyntaxTree _currentSyntaxTree;
#pragma warning disable CS0612 // Type or member is obsolete
private IList<TextChange> _previouslyDiscarded = new List<TextChange>();
#pragma warning restore CS0612 // Type or member is obsolete
public BackgroundThread(MainThreadState main, RazorTemplateEngine templateEngine, string fileName)
{
@ -321,7 +337,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
if (!linkedCancel.IsCancellationRequested)
{
// Collect ALL changes
#pragma warning disable CS0612 // Type or member is obsolete
List<TextChange> allChanges;
#pragma warning restore CS0612 // Type or member is obsolete
if (_previouslyDiscarded != null)
{
@ -411,14 +429,19 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
private class WorkParcel
{
#pragma warning disable CS0612 // Type or member is obsolete
public WorkParcel(IList<TextChange> changes, CancellationToken cancelToken)
#pragma warning restore CS0612 // Type or member is obsolete
{
Changes = changes;
CancelToken = cancelToken;
}
public CancellationToken CancelToken { get; private set; }
#pragma warning disable CS0612 // Type or member is obsolete
public IList<TextChange> Changes { get; private set; }
#pragma warning restore CS0612 // Type or member is obsolete
}
}
}
#pragma warning restore CS0618

View File

@ -102,9 +102,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return current as Span;
}
public virtual Span LocateOwner(TextChange change) => LocateOwner(change, Children);
public virtual Span LocateOwner(SourceChange change) => LocateOwner(change, Children);
protected static Span LocateOwner(TextChange change, IEnumerable<SyntaxTreeNode> elements)
protected static Span LocateOwner(SourceChange change, IEnumerable<SyntaxTreeNode> elements)
{
// Ask each child recursively
Span owner = null;
@ -117,7 +117,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
else
{
if (change.OldPosition < span.Start.AbsoluteIndex)
if (change.Span.AbsoluteIndex < span.Start.AbsoluteIndex)
{
// Early escape for cases where changes overlap multiple spans
// In those cases, the span will return false, and we don't want to search the whole tree
@ -134,6 +134,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
return owner;
}
public override string ToString()
{
return string.Format(

View File

@ -20,9 +20,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
/// </summary>
public RazorCodeDocument GeneratorResults { get; set; }
/// <summary>
/// The TextChange which triggered the re-parse
/// </summary>
#pragma warning disable CS0612 // Type or member is obsolete
public TextChange SourceChange { get; set; }
#pragma warning restore CS0612 // Type or member is obsolete
}
}

View File

@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return hashCodeCombiner;
}
protected override PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange)
protected override PartialParseResult CanAcceptChange(Span target, SourceChange change)
{
if (AcceptedCharacters == AcceptedCharacters.Any)
{
@ -75,21 +75,21 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
// 1. '@foo.' -> '@foobaz.'.
// 2. '@foobaz..' -> '@foobaz.bar.'. Includes Sub-cases '@foobaz()..' -> '@foobaz().bar.' etc.
// The key distinction being the double '.' in the second case.
if (IsDotlessCommitInsertion(target, normalizedChange))
if (IsDotlessCommitInsertion(target, change))
{
return HandleDotlessCommitInsertion(target);
}
if (IsAcceptableIdentifierReplacement(target, normalizedChange))
if (IsAcceptableIdentifierReplacement(target, change))
{
return TryAcceptChange(target, normalizedChange);
return TryAcceptChange(target, change);
}
if (IsAcceptableReplace(target, normalizedChange))
if (IsAcceptableReplace(target, change))
{
return HandleReplacement(target, normalizedChange);
return HandleReplacement(target, change);
}
var changeRelativePosition = normalizedChange.OldPosition - target.Start.AbsoluteIndex;
var changeRelativePosition = change.Span.AbsoluteIndex - target.Start.AbsoluteIndex;
// Get the edit context
char? lastChar = null;
@ -105,57 +105,57 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
// Accepts cases when insertions are made at the end of a span or '.' is inserted within a span.
if (IsAcceptableInsertion(target, normalizedChange))
if (IsAcceptableInsertion(target, change))
{
// Handle the insertion
return HandleInsertion(target, lastChar.Value, normalizedChange);
return HandleInsertion(target, lastChar.Value, change);
}
if (IsAcceptableDeletion(target, normalizedChange))
if (IsAcceptableDeletion(target, change))
{
return HandleDeletion(target, lastChar.Value, normalizedChange);
return HandleDeletion(target, lastChar.Value, change);
}
return PartialParseResult.Rejected;
}
// A dotless commit is the process of inserting a '.' with an intellisense selection.
private static bool IsDotlessCommitInsertion(Span target, TextChange change)
private static bool IsDotlessCommitInsertion(Span target, SourceChange change)
{
return IsNewDotlessCommitInsertion(target, change) || IsSecondaryDotlessCommitInsertion(target, change);
}
// Completing 'DateTime' in intellisense with a '.' could result in: '@DateT' -> '@DateT.' -> '@DateTime.' which is accepted.
private static bool IsNewDotlessCommitInsertion(Span target, TextChange change)
private static bool IsNewDotlessCommitInsertion(Span target, SourceChange change)
{
return !IsAtEndOfSpan(target, change) &&
change.NewPosition > 0 &&
change.NewLength > 0 &&
change.Span.AbsoluteIndex > 0 &&
change.NewText.Length > 0 &&
target.Content.Last() == '.' &&
ParserHelpers.IsIdentifier(change.NewText, requireIdentifierStart: false) &&
(change.OldLength == 0 || ParserHelpers.IsIdentifier(change.OldText, requireIdentifierStart: false));
(change.Span.Length == 0 || ParserHelpers.IsIdentifier(change.GetOriginalText(target), requireIdentifierStart: false));
}
// Once a dotless commit has been performed you then have something like '@DateTime.'. This scenario is used to detect the
// situation when you try to perform another dotless commit resulting in a textchange with '..'. Completing 'DateTime.Now'
// in intellisense with a '.' could result in: '@DateTime.' -> '@DateTime..' -> '@DateTime.Now.' which is accepted.
private static bool IsSecondaryDotlessCommitInsertion(Span target, TextChange change)
private static bool IsSecondaryDotlessCommitInsertion(Span target, SourceChange change)
{
// Do not need to worry about other punctuation, just looking for double '.' (after change)
return change.NewLength == 1 &&
return change.NewText.Length == 1 &&
change.NewText == "." &&
!string.IsNullOrEmpty(target.Content) &&
target.Content.Last() == '.' &&
change.NewText == "." &&
change.OldLength == 0;
change.Span.Length == 0;
}
private static bool IsAcceptableReplace(Span target, TextChange change)
private static bool IsAcceptableReplace(Span target, SourceChange change)
{
return IsEndReplace(target, change) ||
(change.IsReplace && RemainingIsWhitespace(target, change));
}
private bool IsAcceptableIdentifierReplacement(Span target, TextChange change)
private bool IsAcceptableIdentifierReplacement(Span target, SourceChange change)
{
if (!change.IsReplace)
{
@ -174,15 +174,15 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
var symbolStartIndex = symbol.Start.AbsoluteIndex;
var symbolEndIndex = symbolStartIndex + symbol.Content.Length;
// We're looking for the first symbol that contains the TextChange.
if (symbolEndIndex > change.OldPosition)
// We're looking for the first symbol that contains the SourceChange.
if (symbolEndIndex > change.Span.AbsoluteIndex)
{
if (symbolEndIndex >= change.OldPosition + change.OldLength && symbol.Type == CSharpSymbolType.Identifier)
if (symbolEndIndex >= change.Span.AbsoluteIndex + change.Span.Length && symbol.Type == CSharpSymbolType.Identifier)
{
// The symbol we're changing happens to be an identifier. Need to check if its transformed state is also one.
// We do this transformation logic to capture the case that the new text change happens to not be an identifier;
// i.e. "5". Alone, it's numeric, within an identifier it's classified as identifier.
var transformedContent = change.ApplyChange(symbol.Content, symbolStartIndex);
var transformedContent = change.GetEditedContent(symbol.Content, change.Span.AbsoluteIndex - symbolStartIndex);
var newSymbols = Tokenizer(transformedContent);
if (newSymbols.Count() != 1)
@ -208,14 +208,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return false;
}
private static bool IsAcceptableDeletion(Span target, TextChange change)
private static bool IsAcceptableDeletion(Span target, SourceChange change)
{
return IsEndDeletion(target, change) ||
(change.IsDelete && RemainingIsWhitespace(target, change));
}
// Acceptable insertions can occur at the end of a span or when a '.' is inserted within a span.
private static bool IsAcceptableInsertion(Span target, TextChange change)
private static bool IsAcceptableInsertion(Span target, SourceChange change)
{
return change.IsInsert &&
(IsAcceptableEndInsertion(target, change) ||
@ -223,7 +223,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
// Accepts character insertions at the end of spans. AKA: '@foo' -> '@fooo' or '@foo' -> '@foo ' etc.
private static bool IsAcceptableEndInsertion(Span target, TextChange change)
private static bool IsAcceptableEndInsertion(Span target, SourceChange change)
{
Debug.Assert(change.IsInsert);
@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
// Accepts '.' insertions in the middle of spans. Ex: '@foo.baz.bar' -> '@foo..baz.bar'
// This is meant to allow intellisense when editing a span.
private static bool IsAcceptableInnerInsertion(Span target, TextChange change)
private static bool IsAcceptableInnerInsertion(Span target, SourceChange change)
{
Debug.Assert(change.IsInsert);
@ -241,13 +241,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
// This case will fail if the IsAcceptableEndInsertion does not capture an end insertion correctly.
Debug.Assert(!IsAtEndOfSpan(target, change));
return change.NewPosition > 0 &&
return change.Span.AbsoluteIndex > 0 &&
change.NewText == ".";
}
private static bool RemainingIsWhitespace(Span target, TextChange change)
private static bool RemainingIsWhitespace(Span target, SourceChange change)
{
var offset = (change.OldPosition - target.Start.AbsoluteIndex) + change.OldLength;
var offset = (change.Span.AbsoluteIndex - target.Start.AbsoluteIndex) + change.Span.Length;
return string.IsNullOrWhiteSpace(target.Content.Substring(offset));
}
@ -261,14 +261,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return result;
}
private PartialParseResult HandleReplacement(Span target, TextChange change)
private PartialParseResult HandleReplacement(Span target, SourceChange change)
{
// Special Case for IntelliSense commits.
// When IntelliSense commits, we get two changes (for example user typed "Date", then committed "DateTime" by pressing ".")
// 1. Insert "." at the end of this span
// 2. Replace the "Date." at the end of the span with "DateTime."
// We need partial parsing to accept case #2.
var oldText = GetOldText(target, change);
var oldText = change.GetOriginalText(target);
var result = PartialParseResult.Rejected;
if (EndsWithDot(oldText) && EndsWithDot(change.NewText))
@ -282,7 +282,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return result;
}
private PartialParseResult HandleDeletion(Span target, char previousChar, TextChange change)
private PartialParseResult HandleDeletion(Span target, char previousChar, SourceChange change)
{
// What's left after deleting?
if (previousChar == '.')
@ -299,7 +299,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
}
private PartialParseResult HandleInsertion(Span target, char previousChar, TextChange change)
private PartialParseResult HandleInsertion(Span target, char previousChar, SourceChange change)
{
// What are we inserting after?
if (previousChar == '.')
@ -316,7 +316,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
}
private PartialParseResult HandleInsertionAfterIdPart(Span target, TextChange change)
private PartialParseResult HandleInsertionAfterIdPart(Span target, SourceChange change)
{
// If the insertion is a full identifier part, accept it
if (ParserHelpers.IsIdentifier(change.NewText, requireIdentifierStart: false))
@ -346,7 +346,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
content.Take(content.Length - 1).All(ParserHelpers.IsIdentifierPart));
}
private PartialParseResult HandleInsertionAfterDot(Span target, TextChange change)
private PartialParseResult HandleInsertionAfterDot(Span target, SourceChange change)
{
// If the insertion is a full identifier or another dot, accept it
if (ParserHelpers.IsIdentifier(change.NewText) || change.NewText == ".")
@ -356,9 +356,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return PartialParseResult.Rejected;
}
private PartialParseResult TryAcceptChange(Span target, TextChange change, PartialParseResult acceptResult = PartialParseResult.Accepted)
private PartialParseResult TryAcceptChange(Span target, SourceChange change, PartialParseResult acceptResult = PartialParseResult.Accepted)
{
var content = change.ApplyChange(target.Content, target.Start.AbsoluteIndex);
var content = change.GetEditedContent(target);
if (StartsWithKeyword(content))
{
return PartialParseResult.Rejected | PartialParseResult.SpanContextChanged;

View File

@ -3,8 +3,8 @@
using System;
using System.Diagnostics;
using System.IO;
#pragma warning disable CS0618 // TextChange is Obsolete
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
/// <summary>
@ -118,6 +118,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return null;
}
// Type or member is obsolete
/// <summary>
/// Determines if a change will cause a structural change to the document and if not, applies it to the
/// existing tree. If a structural change would occur, automatically starts a reparse.
@ -128,7 +129,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
/// </remarks>
/// <param name="change">The change to apply to the parse tree.</param>
/// <returns>A <see cref="PartialParseResult"/> value indicating the result of the incremental parse.</returns>
#pragma warning disable CS0612
public virtual PartialParseResult CheckForStructureChanges(TextChange change)
#pragma warning restore CS0612 // Type or member is obsolete
{
var result = PartialParseResult.Rejected;
@ -171,14 +174,18 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
}
#pragma warning disable CS0612 // Type or member is obsolete
private PartialParseResult TryPartialParse(TextChange change)
#pragma warning restore CS0612 // Type or member is obsolete
{
var sourceChange = change.AsSourceChange();
var result = PartialParseResult.Rejected;
// Try the last change owner
if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change))
if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, sourceChange))
{
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, sourceChange);
result = editResult.Result;
if ((editResult.Result & PartialParseResult.Rejected) != PartialParseResult.Rejected)
{
@ -189,7 +196,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
// Locate the span responsible for this change
_lastChangeOwner = CurrentSyntaxTree.Root.LocateOwner(change);
_lastChangeOwner = CurrentSyntaxTree.Root.LocateOwner(sourceChange);
if (LastResultProvisional)
{
@ -198,7 +205,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
else if (_lastChangeOwner != null)
{
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, sourceChange);
result = editResult.Result;
if ((editResult.Result & PartialParseResult.Rejected) != PartialParseResult.Rejected)
{
@ -258,3 +265,4 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
}
}
#pragma warning restore CS0618

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
@ -30,49 +31,48 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return new SpanEditHandler(tokenizer);
}
public virtual EditResult ApplyChange(Span target, TextChange change)
public virtual EditResult ApplyChange(Span target, SourceChange change)
{
return ApplyChange(target, change, force: false);
}
public virtual EditResult ApplyChange(Span target, TextChange change, bool force)
public virtual EditResult ApplyChange(Span target, SourceChange change, bool force)
{
var result = PartialParseResult.Accepted;
var normalized = change.Normalize();
if (!force)
{
result = CanAcceptChange(target, normalized);
result = CanAcceptChange(target, change);
}
// If the change is accepted then apply the change
if ((result & PartialParseResult.Accepted) == PartialParseResult.Accepted)
{
return new EditResult(result, UpdateSpan(target, normalized));
return new EditResult(result, UpdateSpan(target, change));
}
return new EditResult(result, new SpanBuilder(target));
}
public virtual bool OwnsChange(Span target, TextChange change)
public virtual bool OwnsChange(Span target, SourceChange change)
{
var end = target.Start.AbsoluteIndex + target.Length;
var changeOldEnd = change.OldPosition + change.OldLength;
return change.OldPosition >= target.Start.AbsoluteIndex &&
var changeOldEnd = change.Span.AbsoluteIndex + change.Span.Length;
return change.Span.AbsoluteIndex >= target.Start.AbsoluteIndex &&
(changeOldEnd < end || (changeOldEnd == end && AcceptedCharacters != AcceptedCharacters.None));
}
protected virtual PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange)
protected virtual PartialParseResult CanAcceptChange(Span target, SourceChange change)
{
return PartialParseResult.Rejected;
}
protected virtual SpanBuilder UpdateSpan(Span target, TextChange normalizedChange)
protected virtual SpanBuilder UpdateSpan(Span target, SourceChange change)
{
var newContent = normalizedChange.ApplyChange(target.Content, target.Start.AbsoluteIndex);
var newContent = change.GetEditedContent(target);
var newSpan = new SpanBuilder(target);
newSpan.ClearSymbols();
foreach (ISymbol sym in Tokenizer(newContent))
foreach (var token in Tokenizer(newContent))
{
newSpan.Accept(sym);
newSpan.Accept(token);
}
if (target.Next != null)
{
@ -82,16 +82,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return newSpan;
}
protected internal static bool IsAtEndOfFirstLine(Span target, TextChange change)
protected internal static bool IsAtEndOfFirstLine(Span target, SourceChange change)
{
var endOfFirstLine = target.Content.IndexOfAny(new char[] { (char)0x000d, (char)0x000a, (char)0x2028, (char)0x2029 });
return (endOfFirstLine == -1 || (change.OldPosition - target.Start.AbsoluteIndex) <= endOfFirstLine);
return (endOfFirstLine == -1 || (change.Span.AbsoluteIndex - target.Start.AbsoluteIndex) <= endOfFirstLine);
}
/// <summary>
/// Returns true if the specified change is an insertion of text at the end of this span.
/// </summary>
protected internal static bool IsEndDeletion(Span target, TextChange change)
protected internal static bool IsEndDeletion(Span target, SourceChange change)
{
return change.IsDelete && IsAtEndOfSpan(target, change);
}
@ -99,25 +99,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
/// <summary>
/// Returns true if the specified change is a replacement of text at the end of this span.
/// </summary>
protected internal static bool IsEndReplace(Span target, TextChange change)
protected internal static bool IsEndReplace(Span target, SourceChange change)
{
return change.IsReplace && IsAtEndOfSpan(target, change);
}
protected internal static bool IsAtEndOfSpan(Span target, TextChange change)
protected internal static bool IsAtEndOfSpan(Span target, SourceChange change)
{
return (change.OldPosition + change.OldLength) == (target.Start.AbsoluteIndex + target.Length);
}
/// <summary>
/// Returns the old text referenced by the change.
/// </summary>
/// <remarks>
/// If the content has already been updated by applying the change, this data will be _invalid_
/// </remarks>
protected internal static string GetOldText(Span target, TextChange change)
{
return target.Content.Substring(change.OldPosition - target.Start.AbsoluteIndex, change.OldLength);
return (change.Span.AbsoluteIndex + change.Span.Length) == (target.Start.AbsoluteIndex + target.Length);
}
public override string ToString()

View File

@ -120,9 +120,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
}
public override Span LocateOwner(TextChange change)
public override Span LocateOwner(SourceChange change)
{
var oldPosition = change.OldPosition;
var oldPosition = change.Span.AbsoluteIndex;
if (oldPosition < Start.AbsoluteIndex)
{
// Change occurs prior to the TagHelper.

View File

@ -9,6 +9,7 @@ using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
[Obsolete] // Superseded by SourceChange
public struct TextChange
{
private string _newText;
@ -127,6 +128,19 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
get { return OldLength > 0 && NewLength > 0; }
}
public SourceChange AsSourceChange()
{
var normalized = Normalize();
return new SourceChange(
new SourceSpan(
filePath: null,
absoluteIndex: normalized.OldPosition,
lineIndex: -1,
characterIndex: -1,
length: normalized.OldLength),
normalized.NewText);
}
public override bool Equals(object obj)
{
if (!(obj is TextChange))
@ -156,15 +170,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return hashCodeCombiner;
}
public string ApplyChange(string content, int changeOffset)
{
var changeRelativePosition = OldPosition - changeOffset;
Debug.Assert(changeRelativePosition >= 0);
return content.Remove(changeRelativePosition, OldLength)
.Insert(changeRelativePosition, NewText);
}
public override string ToString()
{
return string.Format(CultureInfo.CurrentCulture, "({0}:{1}) \"{3}\" -> ({0}:{2}) \"{4}\"", OldPosition, OldLength, NewLength, OldText, NewText);

View File

@ -374,6 +374,20 @@ namespace Microsoft.AspNetCore.Razor.Language
internal static string FormatInvalidTargetedTagNameNullOrWhitespace()
=> GetString("InvalidTargetedTagNameNullOrWhitespace");
/// <summary>
/// The node '{0}' is not the owner of change '{1}'.
/// </summary>
internal static string InvalidOperation_SpanIsNotChangeOwner
{
get => GetString("InvalidOperation_SpanIsNotChangeOwner");
}
/// <summary>
/// The node '{0}' is not the owner of change '{1}'.
/// </summary>
internal static string FormatInvalidOperation_SpanIsNotChangeOwner(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidOperation_SpanIsNotChangeOwner"), p0, p1);
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -195,4 +195,7 @@
<data name="InvalidTargetedTagNameNullOrWhitespace" xml:space="preserve">
<value>Targeted tag name cannot be null or whitespace.</value>
</data>
<data name="InvalidOperation_SpanIsNotChangeOwner" xml:space="preserve">
<value>The node '{0}' is not the owner of change '{1}'.</value>
</data>
</root>

View File

@ -0,0 +1,138 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Language
{
public sealed class SourceChange : IEquatable<SourceChange>
{
public SourceChange(int absoluteIndex, int length, string newText)
{
if (absoluteIndex < 0)
{
throw new ArgumentOutOfRangeException(nameof(absoluteIndex));
}
if (length < 0)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
if (newText == null)
{
throw new ArgumentNullException(nameof(newText));
}
Span = new SourceSpan(absoluteIndex, length);
NewText = newText;
}
public SourceChange(SourceSpan span, string newText)
{
if (newText == null)
{
throw new ArgumentNullException(nameof(newText));
}
Span = span;
NewText = newText;
}
public bool IsDelete => Span.Length > 0 && NewText.Length == 0;
public bool IsInsert => Span.Length == 0 && NewText.Length > 0;
public bool IsReplace => Span.Length > 0 && NewText.Length > 0;
public SourceSpan Span { get; }
public string NewText { get; }
internal string GetEditedContent(Span span)
{
if (span == null)
{
throw new ArgumentNullException(nameof(span));
}
var offset = GetOffset(span);
return GetEditedContent(span.Content, offset);
}
internal string GetEditedContent(string text, int offset)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
return text.Remove(offset, Span.Length).Insert(offset, NewText);
}
internal int GetOffset(Span span)
{
if (span == null)
{
throw new ArgumentNullException(nameof(span));
}
var start = Span.AbsoluteIndex;
var end = Span.AbsoluteIndex + Span.Length;
if (start < span.Start.AbsoluteIndex ||
start > span.Start.AbsoluteIndex + span.Length ||
end < span.Start.AbsoluteIndex ||
end > span.Start.AbsoluteIndex + span.Length)
{
throw new InvalidOperationException(Resources.FormatInvalidOperation_SpanIsNotChangeOwner(span, this));
}
return start - span.Start.AbsoluteIndex;
}
internal string GetOriginalText(Span span)
{
if (span == null)
{
throw new ArgumentNullException(nameof(span));
}
if (span.Length == 0)
{
return string.Empty;
}
var offset = GetOffset(span);
return span.Content.Substring(offset, Span.Length);
}
public bool Equals(SourceChange other)
{
return
other != null &&
Span.Equals(other.Span) &&
string.Equals(NewText, other.NewText, StringComparison.Ordinal);
}
public override bool Equals(object obj)
{
return Equals(obj as SourceChange);
}
public override int GetHashCode()
{
var hash = new HashCodeCombiner();
hash.Add(Span);
hash.Add(NewText, StringComparer.Ordinal);
return hash;
}
public override string ToString()
{
return Span.ToString() + " : " + NewText;
}
}
}

View File

@ -9,6 +9,11 @@ namespace Microsoft.AspNetCore.Razor.Language
{
public struct SourceSpan : IEquatable<SourceSpan>
{
public SourceSpan(int absoluteIndex, int length)
: this(null, absoluteIndex, -1, -1, length)
{
}
public SourceSpan(SourceLocation location, int contentLength)
: this(location.FilePath, location.AbsoluteIndex, location.LineIndex, location.CharacterIndex, contentLength)
{

View File

@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor
{
internal static class TextChangeExtensions
{
public static SourceChange AsSourceChange(this TextChange textChange)
{
if (textChange == null)
{
throw new ArgumentNullException(nameof(textChange));
}
return new SourceChange(textChange.Span.AsSourceSpan(), textChange.NewText);
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor
{
internal static class TextSpanExtensions
{
public static SourceSpan AsSourceSpan(this TextSpan textSpan)
{
return new SourceSpan(filePath: null, absoluteIndex: textSpan.Start, lineIndex: -1, characterIndex: -1, length: textSpan.Length);
}
}
}

View File

@ -171,8 +171,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
var trackingPoint = line.Snapshot.CreateTrackingPoint(line.End, PointTrackingMode.Negative);
var previousLineEnd = trackingPoint.GetPosition(syntaxTreeSnapshot);
var razorBuffer = new ShimTextBufferAdapter(syntaxTreeSnapshot);
var simulatedChange = new TextChange(previousLineEnd, 0, razorBuffer, previousLineEnd, 0, razorBuffer);
var simulatedChange = new SourceChange(previousLineEnd, 0, string.Empty);
var owningSpan = LocateOwner(syntaxTree.Root, simulatedChange);
int? desiredIndentation = null;
@ -223,20 +222,20 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
return desiredIndentation;
}
private Span LocateOwner(Block root, TextChange change)
private Span LocateOwner(Block root, SourceChange change)
{
// Ask each child recursively
Span owner = null;
foreach (SyntaxTreeNode element in root.Children)
{
if (element.Start.AbsoluteIndex > change.OldPosition)
if (element.Start.AbsoluteIndex > change.Span.AbsoluteIndex)
{
// too far
break;
}
int elementLen = element.Length;
if (element.Start.AbsoluteIndex + elementLen < change.OldPosition)
if (element.Start.AbsoluteIndex + elementLen < change.Span.AbsoluteIndex)
{
// not far enough
continue;
@ -246,7 +245,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
{
Block block = element as Block;
if (element.Start.AbsoluteIndex + elementLen == change.OldPosition)
if (element.Start.AbsoluteIndex + elementLen == change.Span.AbsoluteIndex)
{
Span lastDescendant = block.FindLastDescendentSpan();
if ((lastDescendant == null) && (block is TagHelperBlock))
@ -306,14 +305,14 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
{
Block sourceStartTag = tagHelperNode.SourceStartTag;
Block sourceEndTag = tagHelperNode.SourceEndTag;
if ((sourceStartTag.Start.AbsoluteIndex <= change.OldPosition) &&
(sourceStartTag.Start.AbsoluteIndex + sourceStartTag.Length >= change.OldPosition))
if ((sourceStartTag.Start.AbsoluteIndex <= change.Span.AbsoluteIndex) &&
(sourceStartTag.Start.AbsoluteIndex + sourceStartTag.Length >= change.Span.AbsoluteIndex))
{
// intersects the start tag
return LocateOwner(sourceStartTag, change);
}
else if ((sourceEndTag.Start.AbsoluteIndex <= change.OldPosition) &&
(sourceEndTag.Start.AbsoluteIndex + sourceEndTag.Length >= change.OldPosition))
else if ((sourceEndTag.Start.AbsoluteIndex <= change.Span.AbsoluteIndex) &&
(sourceEndTag.Start.AbsoluteIndex + sourceEndTag.Length >= change.Span.AbsoluteIndex))
{
// intersects the end tag
return LocateOwner(sourceEndTag, change);
@ -346,88 +345,5 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
return indentLevel;
}
private class ShimTextBufferAdapter : ITextBuffer
{
public ITextSnapshot Snapshot { get; private set; }
private int _position;
private string _cachedText;
private int _cachedPos;
public ShimTextBufferAdapter(ITextSnapshot snapshot)
{
Snapshot = snapshot;
_cachedPos = -1;
}
#region IRazorTextBuffer
int ITextBuffer.Length
{
get { return Length; }
}
int ITextBuffer.Position
{
get { return _position; }
set { _position = value; }
}
int ITextBuffer.Read()
{
return Read();
}
int ITextBuffer.Peek()
{
return Peek();
}
#endregion
#region private methods
private int Length
{
get { return Snapshot.Length; }
}
private int Read()
{
if (_position >= Snapshot.Length)
{
return -1;
}
int readVal = ReadChar();
_position = _position + 1;
return readVal;
}
private int Peek()
{
if (_position >= Snapshot.Length)
{
return -1;
}
return ReadChar();
}
private int ReadChar()
{
if ((_cachedPos < 0) || (_position < _cachedPos) || (_position >= _cachedPos + _cachedText.Length))
{
_cachedPos = _position;
int cachedLen = Math.Min(1024, Snapshot.Length - _cachedPos);
_cachedText = Snapshot.GetText(_cachedPos, cachedLen);
}
return _cachedText[_position - _cachedPos];
}
#endregion
}
}
}

View File

@ -7,6 +7,7 @@ using System.Threading;
using Microsoft.AspNetCore.Testing;
using Xunit;
#pragma warning disable CS0612 // Type or member is obsolete
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
public class RazorEditorParserTest
@ -1448,3 +1449,4 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
}
}
#pragma warning restore CS0612 // Type or member is obsolete

View File

@ -5,6 +5,7 @@ using System;
using Moq;
using Xunit;
#pragma warning disable CS0612 // Type or member is obsolete
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
public class TextChangeTest
@ -224,51 +225,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
Assert.Equal("bb", oldText);
}
[Fact]
public void ApplyChangeWithInsertedTextReturnsNewContentWithChangeApplied()
{
// Arrange
var newBuffer = new StringTextBuffer("test");
var oldBuffer = new StringTextBuffer("");
var textChange = new TextChange(0, 0, oldBuffer, 3, newBuffer);
// Act
var text = textChange.ApplyChange("abcd", 0);
// Assert
Assert.Equal("tesabcd", text);
}
[Fact]
public void ApplyChangeWithRemovedTextReturnsNewContentWithChangeApplied()
{
// Arrange
var newBuffer = new StringTextBuffer("abcdefg");
var oldBuffer = new StringTextBuffer("");
var textChange = new TextChange(1, 1, oldBuffer, 0, newBuffer);
// Act
var text = textChange.ApplyChange("abcdefg", 1);
// Assert
Assert.Equal("bcdefg", text);
}
[Fact]
public void ApplyChangeWithReplacedTextReturnsNewContentWithChangeApplied()
{
// Arrange
var newBuffer = new StringTextBuffer("abcdefg");
var oldBuffer = new StringTextBuffer("");
var textChange = new TextChange(1, 1, oldBuffer, 2, newBuffer);
// Act
var text = textChange.ApplyChange("abcdefg", 1);
// Assert
Assert.Equal("bcbcdefg", text);
}
[Fact]
public void NormalizeFixesUpIntelliSenseStyleReplacements()
{
@ -315,3 +271,4 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
}
}
#pragma warning restore CS0612 // Type or member is obsolete

View File

@ -0,0 +1,203 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Language
{
public class SourceChangeTest
{
[Fact]
public void SourceChange_ConstructorSetsDefaults_WhenNotProvided()
{
// Arrange & Act
var change = new SourceChange(15, 7, "Hello");
// Assert
Assert.Equal(15, change.Span.AbsoluteIndex);
Assert.Equal(-1, change.Span.CharacterIndex);
Assert.Null(change.Span.FilePath);
Assert.Equal(7, change.Span.Length);
Assert.Equal(-1, change.Span.LineIndex);
Assert.Equal("Hello", change.NewText);
}
[Fact]
public void IsDelete_IsTrue_WhenOldLengthIsPositive_AndNewLengthIsZero()
{
// Arrange & Act
var change = new SourceChange(3, 5, string.Empty);
// Assert
Assert.True(change.IsDelete);
}
[Fact]
public void IsInsert_IsTrue_WhenOldLengthIsZero_AndNewLengthIsPositive()
{
// Arrange & Act
var change = new SourceChange(3, 0, "Hello");
// Assert
Assert.True(change.IsInsert);
}
[Fact]
public void IsReplace_IsTrue_WhenOldLengthIsPositive_AndNewLengthIsPositive()
{
// Arrange & Act
var change = new SourceChange(3, 5, "Hello");
// Assert
Assert.True(change.IsReplace);
}
[Fact]
public void GetEditedContent_ForDelete_ReturnsNewContent()
{
// Arrange
var text = "Hello, World";
var change = new SourceChange(2, 2, string.Empty);
// Act
var result = change.GetEditedContent(text, 1);
// Act
Assert.Equal("Hlo, World", result);
}
[Fact]
public void GetEditedContent_ForInsert_ReturnsNewContent()
{
// Arrange
var text = "Hello, World";
var change = new SourceChange(2, 0, "heyo");
// Act
var result = change.GetEditedContent(text, 1);
// Act
Assert.Equal("Hheyoello, World", result);
}
[Fact]
public void GetEditedContent_ForReplace_ReturnsNewContent()
{
// Arrange
var text = "Hello, World";
var change = new SourceChange(2, 2, "heyo");
// Act
var result = change.GetEditedContent(text, 1);
// Act
Assert.Equal("Hheyolo, World", result);
}
[Fact]
public void GetEditedContent_Span_ReturnsNewContent()
{
// Arrange
var builder = new SpanBuilder(new SourceLocation(0, 0, 0));
builder.Accept(new RawTextSymbol(new SourceLocation(0, 0, 0), "Hello, "));
builder.Accept(new RawTextSymbol(new SourceLocation(7, 0, 7), "World"));
var span = new Span(builder);
var change = new SourceChange(2, 2, "heyo");
// Act
var result = change.GetEditedContent(span);
// Act
Assert.Equal("Heheyoo, World", result);
}
[Fact]
public void GetOffSet_SpanIsOwner_ReturnsOffset()
{
// Arrange
var builder = new SpanBuilder(new SourceLocation(13, 0, 0));
builder.Accept(new RawTextSymbol(new SourceLocation(13, 0, 13), "Hello, "));
builder.Accept(new RawTextSymbol(new SourceLocation(20, 0, 20), "World"));
var span = new Span(builder);
var change = new SourceChange(15, 2, "heyo");
// Act
var result = change.GetOffset(span);
// Act
Assert.Equal(2, result);
}
[Theory]
[InlineData(12, 2)]
[InlineData(12, 14)]
[InlineData(13, 13)]
[InlineData(13, 13)]
[InlineData(20, 1)]
[InlineData(21, 0)]
public void GetOffSet_SpanIsNotOwnerOfChange_ThrowsException(int absoluteIndex, int length)
{
// Arrange
var builder = new SpanBuilder(new SourceLocation(13, 0, 0));
builder.Accept(new RawTextSymbol(new SourceLocation(13, 0, 13), "Hello, "));
builder.Accept(new RawTextSymbol(new SourceLocation(20, 0, 20), "World"));
var span = new Span(builder);
var change = new SourceChange(12, 2, "heyo");
var expected = $"The node '{span}' is not the owner of change '{change}'.";
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => { change.GetOffset(span); });
Assert.Equal(expected, exception.Message);
}
[Fact]
public void GetOrigninalText_SpanIsOwner_ReturnsContent()
{
// Arrange
var builder = new SpanBuilder(new SourceLocation(13, 0, 0));
builder.Accept(new RawTextSymbol(new SourceLocation(13, 0, 13), "Hello, "));
builder.Accept(new RawTextSymbol(new SourceLocation(20, 0, 20), "World"));
var span = new Span(builder);
var change = new SourceChange(15, 2, "heyo");
// Act
var result = change.GetOriginalText(span);
// Act
Assert.Equal("ll", result);
}
[Fact]
public void GetOrigninalText_SpanIsOwner_ReturnsContent_ZeroLengthSpan()
{
// Arrange
var builder = new SpanBuilder(new SourceLocation(13, 0, 0));
builder.Accept(new RawTextSymbol(new SourceLocation(13, 0, 13), "Hello, "));
builder.Accept(new RawTextSymbol(new SourceLocation(20, 0, 20), "World"));
var span = new Span(builder);
var change = new SourceChange(15, 0, "heyo");
// Act
var result = change.GetOriginalText(span);
// Act
Assert.Equal(string.Empty, result);
}
}
}