From e15d1be6167ec76f31a5f7fa4e61e6bbbab3fab3 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 3 May 2017 00:49:40 -0700 Subject: [PATCH] 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. --- .../Legacy/AutoCompleteEditHandler.cs | 8 +- .../Legacy/BackgroundParser.cs | 39 +++- .../Legacy/Block.cs | 7 +- .../Legacy/DocumentParseCompleteEventArgs.cs | 3 + .../Legacy/ImplicitExpressionEditHandler.cs | 82 +++---- .../Legacy/RazorEditorParser.cs | 18 +- .../Legacy/SpanEditHandler.cs | 49 ++--- .../Legacy/TagHelperBlock.cs | 4 +- .../Legacy/TextChange.cs | 23 +- .../Properties/Resources.Designer.cs | 14 ++ .../Resources.resx | 3 + .../SourceChange.cs | 138 ++++++++++++ .../SourceSpan.cs | 5 + .../TextChangeExtensions.cs | 22 ++ .../TextSpanExtensions.cs | 16 ++ .../DefaultRazorSyntaxFactsService.cs | 102 +-------- .../Legacy/RazorEditorParserTest.cs | 2 + .../Legacy/TextChangeTest.cs | 47 +--- .../SourceChangeTest.cs | 203 ++++++++++++++++++ 19 files changed, 545 insertions(+), 240 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Language/SourceChange.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor/TextChangeExtensions.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor/TextSpanExtensions.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Language.Test/SourceChangeTest.cs diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs index c9d99b6a76..48bfa5dbc1 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs @@ -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; diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/BackgroundParser.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/BackgroundParser.cs index bbbeaa9e53..bc9a97a43f 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/BackgroundParser.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/BackgroundParser.cs @@ -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 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 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 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 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 _changes = new List(); +#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(); +#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 _previouslyDiscarded = new List(); +#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 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 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 Changes { get; private set; } +#pragma warning restore CS0612 // Type or member is obsolete } } } +#pragma warning restore CS0618 diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs index 91b5397cfb..39fc09ea9e 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs @@ -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 elements) + protected static Span LocateOwner(SourceChange change, IEnumerable 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( diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/DocumentParseCompleteEventArgs.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/DocumentParseCompleteEventArgs.cs index 475982401b..3a8d1e09fe 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/DocumentParseCompleteEventArgs.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/DocumentParseCompleteEventArgs.cs @@ -20,9 +20,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy /// public RazorCodeDocument GeneratorResults { get; set; } + /// /// The TextChange which triggered the re-parse /// +#pragma warning disable CS0612 // Type or member is obsolete public TextChange SourceChange { get; set; } +#pragma warning restore CS0612 // Type or member is obsolete } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs index 0d738627c7..d178dd82e9 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs @@ -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; diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/RazorEditorParser.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/RazorEditorParser.cs index 8baa27616f..b7ce29e3ab 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/RazorEditorParser.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/RazorEditorParser.cs @@ -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 { /// @@ -118,6 +118,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return null; } + // Type or member is obsolete /// /// 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 /// /// The change to apply to the parse tree. /// A value indicating the result of the incremental parse. +#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 diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs index 52ab541b31..90f446bbe6 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs @@ -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); } /// /// Returns true if the specified change is an insertion of text at the end of this span. /// - 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 /// /// Returns true if the specified change is a replacement of text at the end of this span. /// - 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); - } - - /// - /// Returns the old text referenced by the change. - /// - /// - /// If the content has already been updated by applying the change, this data will be _invalid_ - /// - 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() diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlock.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlock.cs index fca60a0b51..9d3f7167be 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlock.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlock.cs @@ -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. diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TextChange.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TextChange.cs index 43a8062bc8..8f9a34ca9f 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TextChange.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TextChange.cs @@ -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); diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs index c748c0a841..8617863c1d 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs @@ -374,6 +374,20 @@ namespace Microsoft.AspNetCore.Razor.Language internal static string FormatInvalidTargetedTagNameNullOrWhitespace() => GetString("InvalidTargetedTagNameNullOrWhitespace"); + /// + /// The node '{0}' is not the owner of change '{1}'. + /// + internal static string InvalidOperation_SpanIsNotChangeOwner + { + get => GetString("InvalidOperation_SpanIsNotChangeOwner"); + } + + /// + /// The node '{0}' is not the owner of change '{1}'. + /// + 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); diff --git a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx index 7040315c9e..8a2d4c4fcf 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx +++ b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx @@ -195,4 +195,7 @@ Targeted tag name cannot be null or whitespace. + + The node '{0}' is not the owner of change '{1}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Language/SourceChange.cs b/src/Microsoft.AspNetCore.Razor.Language/SourceChange.cs new file mode 100644 index 0000000000..75e4b8e44e --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/SourceChange.cs @@ -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 + { + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/SourceSpan.cs b/src/Microsoft.AspNetCore.Razor.Language/SourceSpan.cs index 625d2c4989..3d17ccd3fd 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/SourceSpan.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/SourceSpan.cs @@ -9,6 +9,11 @@ namespace Microsoft.AspNetCore.Razor.Language { public struct SourceSpan : IEquatable { + 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) { diff --git a/src/Microsoft.CodeAnalysis.Razor/TextChangeExtensions.cs b/src/Microsoft.CodeAnalysis.Razor/TextChangeExtensions.cs new file mode 100644 index 0000000000..95fd6ba899 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor/TextChangeExtensions.cs @@ -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); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor/TextSpanExtensions.cs b/src/Microsoft.CodeAnalysis.Razor/TextSpanExtensions.cs new file mode 100644 index 0000000000..32e43de450 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor/TextSpanExtensions.cs @@ -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); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorSyntaxFactsService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorSyntaxFactsService.cs index 7b30bcb41c..a85eeaf970 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorSyntaxFactsService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorSyntaxFactsService.cs @@ -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 - } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/RazorEditorParserTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/RazorEditorParserTest.cs index af34aaaa69..75fd55b0eb 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/RazorEditorParserTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/RazorEditorParserTest.cs @@ -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 diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TextChangeTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TextChangeTest.cs index b9a081787e..1328b9bf9f 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TextChangeTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TextChangeTest.cs @@ -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 \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/SourceChangeTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/SourceChangeTest.cs new file mode 100644 index 0000000000..5423b2553b --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/SourceChangeTest.cs @@ -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(() => { 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); + } + } +}