diff --git a/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs index 5c3f4403e0..6abaa03f5c 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Razor.Language { } - protected override PartialParseResult CanAcceptChange(Span target, SourceChange change) + protected override PartialParseResultInternal CanAcceptChange(Span target, SourceChange change) { if (AcceptedCharacters == AcceptedCharactersInternal.NonWhiteSpace) { @@ -25,11 +25,11 @@ namespace Microsoft.AspNetCore.Razor.Language // Did not modify whitespace, directive format should be the same. // Return provisional so extensible IR/code gen pieces can see the full directive text // once the user stops editing the document. - return PartialParseResult.Accepted | PartialParseResult.Provisional; + return PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional; } } - return PartialParseResult.Rejected; + return PartialParseResultInternal.Rejected; } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs index ae4291f987..bd73bbc1b2 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs @@ -31,16 +31,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public string AutoCompleteString { get; set; } - protected override PartialParseResult CanAcceptChange(Span target, SourceChange change) + protected override PartialParseResultInternal CanAcceptChange(Span target, SourceChange change) { if (((AutoCompleteAtEndOfSpan && IsAtEndOfSpan(target, change)) || IsAtEndOfFirstLine(target, change)) && change.IsInsert && ParserHelpers.IsNewLine(change.NewText) && AutoCompleteString != null) { - return PartialParseResult.Rejected | PartialParseResult.AutoCompleteBlock; + return PartialParseResultInternal.Rejected | PartialParseResultInternal.AutoCompleteBlock; } - return PartialParseResult.Rejected; + return PartialParseResultInternal.Rejected; } public override string ToString() diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs index 351116e090..6aff8dea24 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs @@ -5,13 +5,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { internal class EditResult { - public EditResult(PartialParseResult result, SpanBuilder editedSpan) + public EditResult(PartialParseResultInternal result, SpanBuilder editedSpan) { Result = result; EditedSpan = editedSpan; } - public PartialParseResult Result { get; set; } + public PartialParseResultInternal Result { get; set; } public SpanBuilder EditedSpan { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs index 82b9b5db19..ababd6c7f7 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs @@ -59,11 +59,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return hashCodeCombiner; } - protected override PartialParseResult CanAcceptChange(Span target, SourceChange change) + protected override PartialParseResultInternal CanAcceptChange(Span target, SourceChange change) { if (AcceptedCharacters == AcceptedCharactersInternal.Any) { - return PartialParseResult.Rejected; + return PartialParseResultInternal.Rejected; } // In some editors intellisense insertions are handled as "dotless commits". If an intellisense selection is confirmed @@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy // Don't support 0->1 length edits if (lastChar == null) { - return PartialParseResult.Rejected; + return PartialParseResultInternal.Rejected; } // Accepts cases when insertions are made at the end of a span or '.' is inserted within a span. @@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return HandleDeletion(target, lastChar.Value, change); } - return PartialParseResult.Rejected; + return PartialParseResultInternal.Rejected; } // A dotless commit is the process of inserting a '.' with an intellisense selection. @@ -249,17 +249,17 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return string.IsNullOrWhiteSpace(target.Content.Substring(offset)); } - private PartialParseResult HandleDotlessCommitInsertion(Span target) + private PartialParseResultInternal HandleDotlessCommitInsertion(Span target) { - var result = PartialParseResult.Accepted; + var result = PartialParseResultInternal.Accepted; if (!AcceptTrailingDot && target.Content.LastOrDefault() == '.') { - result |= PartialParseResult.Provisional; + result |= PartialParseResultInternal.Provisional; } return result; } - private PartialParseResult HandleReplacement(Span target, SourceChange change) + private PartialParseResultInternal 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 ".") @@ -268,24 +268,24 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy // We need partial parsing to accept case #2. var oldText = change.GetOriginalText(target); - var result = PartialParseResult.Rejected; + var result = PartialParseResultInternal.Rejected; if (EndsWithDot(oldText) && EndsWithDot(change.NewText)) { - result = PartialParseResult.Accepted; + result = PartialParseResultInternal.Accepted; if (!AcceptTrailingDot) { - result |= PartialParseResult.Provisional; + result |= PartialParseResultInternal.Provisional; } } return result; } - private PartialParseResult HandleDeletion(Span target, char previousChar, SourceChange change) + private PartialParseResultInternal HandleDeletion(Span target, char previousChar, SourceChange change) { // What's left after deleting? if (previousChar == '.') { - return TryAcceptChange(target, change, PartialParseResult.Accepted | PartialParseResult.Provisional); + return TryAcceptChange(target, change, PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional); } else if (ParserHelpers.IsIdentifierPart(previousChar)) { @@ -293,11 +293,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } else { - return PartialParseResult.Rejected; + return PartialParseResultInternal.Rejected; } } - private PartialParseResult HandleInsertion(Span target, char previousChar, SourceChange change) + private PartialParseResultInternal HandleInsertion(Span target, char previousChar, SourceChange change) { // What are we inserting after? if (previousChar == '.') @@ -310,11 +310,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } else { - return PartialParseResult.Rejected; + return PartialParseResultInternal.Rejected; } } - private PartialParseResult HandleInsertionAfterIdPart(Span target, SourceChange change) + private PartialParseResultInternal HandleInsertionAfterIdPart(Span target, SourceChange change) { // If the insertion is a full identifier part, accept it if (ParserHelpers.IsIdentifier(change.NewText, requireIdentifierStart: false)) @@ -324,16 +324,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy else if (EndsWithDot(change.NewText)) { // Accept it, possibly provisionally - var result = PartialParseResult.Accepted; + var result = PartialParseResultInternal.Accepted; if (!AcceptTrailingDot) { - result |= PartialParseResult.Provisional; + result |= PartialParseResultInternal.Provisional; } return TryAcceptChange(target, change, result); } else { - return PartialParseResult.Rejected; + return PartialParseResultInternal.Rejected; } } @@ -344,22 +344,22 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy content.Take(content.Length - 1).All(ParserHelpers.IsIdentifierPart)); } - private PartialParseResult HandleInsertionAfterDot(Span target, SourceChange change) + private PartialParseResultInternal HandleInsertionAfterDot(Span target, SourceChange change) { // If the insertion is a full identifier or another dot, accept it if (ParserHelpers.IsIdentifier(change.NewText) || change.NewText == ".") { return TryAcceptChange(target, change); } - return PartialParseResult.Rejected; + return PartialParseResultInternal.Rejected; } - private PartialParseResult TryAcceptChange(Span target, SourceChange change, PartialParseResult acceptResult = PartialParseResult.Accepted) + private PartialParseResultInternal TryAcceptChange(Span target, SourceChange change, PartialParseResultInternal acceptResult = PartialParseResultInternal.Accepted) { var content = change.GetEditedContent(target); if (StartsWithKeyword(content)) { - return PartialParseResult.Rejected | PartialParseResult.SpanContextChanged; + return PartialParseResultInternal.Rejected | PartialParseResultInternal.SpanContextChanged; } return acceptResult; diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResult.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResultInternal.cs similarity index 98% rename from src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResult.cs rename to src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResultInternal.cs index 0b61553b53..a6a79c01bf 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResult.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResultInternal.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy /// Provisional may NOT be set with Rejected and SpanContextChanged may NOT be set with Accepted. /// [Flags] - internal enum PartialParseResult + internal enum PartialParseResultInternal { /// /// Indicates that the edit could not be accepted and that a reparse is underway. diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs index 4bef77c6f7..dd9511ce70 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs @@ -38,14 +38,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public virtual EditResult ApplyChange(Span target, SourceChange change, bool force) { - var result = PartialParseResult.Accepted; + var result = PartialParseResultInternal.Accepted; if (!force) { result = CanAcceptChange(target, change); } // If the change is accepted then apply the change - if ((result & PartialParseResult.Accepted) == PartialParseResult.Accepted) + if ((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted) { return new EditResult(result, UpdateSpan(target, change)); } @@ -60,9 +60,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy (changeOldEnd < end || (changeOldEnd == end && AcceptedCharacters != AcceptedCharactersInternal.None)); } - protected virtual PartialParseResult CanAcceptChange(Span target, SourceChange change) + protected virtual PartialParseResultInternal CanAcceptChange(Span target, SourceChange change) { - return PartialParseResult.Rejected; + return PartialParseResultInternal.Rejected; } protected virtual SpanBuilder UpdateSpan(Span target, SourceChange change) diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundParser.cs new file mode 100644 index 0000000000..b71f4d8814 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundParser.cs @@ -0,0 +1,419 @@ +// 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 System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + internal class BackgroundParser : IDisposable + { + private MainThreadState _main; + private BackgroundThread _bg; + + public BackgroundParser(RazorTemplateEngine templateEngine, string filePath) + { + _main = new MainThreadState(filePath); + _bg = new BackgroundThread(_main, templateEngine, filePath); + + _main.ResultsReady += (sender, args) => OnResultsReady(args); + } + + /// + /// Fired on the main thread. + /// + public event EventHandler ResultsReady; + + public bool IsIdle + { + get { return _main.IsIdle; } + } + + public void Start() + { + _bg.Start(); + } + + public void Cancel() + { + _main.Cancel(); + } + + public void QueueChange(SourceChange change, ITextSnapshot snapshot) + { + var edit = new Edit(change, snapshot); + _main.QueueChange(edit); + } + + public void Dispose() + { + _main.Cancel(); + } + + public IDisposable SynchronizeMainThreadState() + { + return _main.Lock(); + } + + protected virtual void OnResultsReady(DocumentParseCompleteEventArgs args) + { + var handler = ResultsReady; + if (handler != null) + { + handler(this, args); + } + } + + private static bool TreesAreDifferent(RazorSyntaxTree leftTree, RazorSyntaxTree rightTree, IEnumerable edits, CancellationToken cancelToken) + { + return TreesAreDifferent(leftTree.Root, rightTree.Root, edits.Select(edit => edit.Change), cancelToken); + } + + internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable changes, CancellationToken cancelToken) + { + // 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 (var change in changes) + { + cancelToken.ThrowIfCancellationRequested(); + + var changeOwner = leftTree.LocateOwner(change); + + // Apply the change to the tree + if (changeOwner == null) + { + return true; + } + + var result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true); + changeOwner.ReplaceWith(result.EditedSpan); + } + + // Now compare the trees + var treesDifferent = !leftTree.EquivalentTo(rightTree); + return treesDifferent; + } + + private abstract class ThreadStateBase + { +#if DEBUG + private int _id = -1; +#endif + protected ThreadStateBase() + { + } + + [Conditional("DEBUG")] + protected void SetThreadId(int id) + { +#if DEBUG + _id = id; +#endif + } + + [Conditional("DEBUG")] + protected void EnsureOnThread() + { +#if DEBUG + Debug.Assert(_id != -1, "SetThreadId was never called!"); + Debug.Assert(Thread.CurrentThread.ManagedThreadId == _id, "Called from an unexpected thread!"); +#endif + } + + [Conditional("DEBUG")] + protected void EnsureNotOnThread() + { +#if DEBUG + Debug.Assert(_id != -1, "SetThreadId was never called!"); + Debug.Assert(Thread.CurrentThread.ManagedThreadId != _id, "Called from an unexpected thread!"); +#endif + } + } + + private class MainThreadState : ThreadStateBase, IDisposable + { + private readonly CancellationTokenSource _cancelSource = new CancellationTokenSource(); + private readonly ManualResetEventSlim _hasParcel = new ManualResetEventSlim(false); + private CancellationTokenSource _currentParcelCancelSource; + + private string _fileName; + private readonly object _stateLock = new object(); + private IList _changes = new List(); + + public MainThreadState(string fileName) + { + _fileName = fileName; + + SetThreadId(Thread.CurrentThread.ManagedThreadId); + } + + public event EventHandler ResultsReady; + + public CancellationToken CancelToken + { + get { return _cancelSource.Token; } + } + + public bool IsIdle + { + get + { + lock (_stateLock) + { + return _currentParcelCancelSource == null; + } + } + } + + public void Cancel() + { + EnsureOnThread(); + _cancelSource.Cancel(); + } + + public IDisposable Lock() + { + Monitor.Enter(_stateLock); + return new DisposableAction(() => Monitor.Exit(_stateLock)); + } + + public void QueueChange(Edit edit) + { + EnsureOnThread(); + lock (_stateLock) + { + // CurrentParcel token source is not null ==> There's a parse underway + if (_currentParcelCancelSource != null) + { + _currentParcelCancelSource.Cancel(); + } + + _changes.Add(edit); + _hasParcel.Set(); + } + } + + public WorkParcel GetParcel() + { + EnsureNotOnThread(); // Only the background thread can get a parcel + _hasParcel.Wait(_cancelSource.Token); + _hasParcel.Reset(); + lock (_stateLock) + { + // Create a cancellation source for this parcel + _currentParcelCancelSource = new CancellationTokenSource(); + + var changes = _changes; + _changes = new List(); + return new WorkParcel(changes, _currentParcelCancelSource.Token); + } + } + + public void ReturnParcel(DocumentParseCompleteEventArgs args) + { + lock (_stateLock) + { + // Clear the current parcel cancellation source + if (_currentParcelCancelSource != null) + { + _currentParcelCancelSource.Dispose(); + _currentParcelCancelSource = null; + } + + // If there are things waiting to be parsed, just don't fire the event because we're already out of date + if (_changes.Any()) + { + return; + } + } + var handler = ResultsReady; + if (handler != null) + { + handler(this, args); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_currentParcelCancelSource != null) + { + _currentParcelCancelSource.Dispose(); + _currentParcelCancelSource = null; + } + _cancelSource.Dispose(); + _hasParcel.Dispose(); + } + } + } + + private class BackgroundThread : ThreadStateBase + { + private MainThreadState _main; + private Thread _backgroundThread; + private CancellationToken _shutdownToken; + private RazorTemplateEngine _templateEngine; + private string _filePath; + private RazorSyntaxTree _currentSyntaxTree; + private IList _previouslyDiscarded = new List(); + + public BackgroundThread(MainThreadState main, RazorTemplateEngine templateEngine, string fileName) + { + // Run on MAIN thread! + _main = main; + _shutdownToken = _main.CancelToken; + _templateEngine = templateEngine; + _filePath = fileName; + + _backgroundThread = new Thread(WorkerLoop); + SetThreadId(_backgroundThread.ManagedThreadId); + } + + // **** ANY THREAD **** + public void Start() + { + _backgroundThread.Start(); + } + + // **** BACKGROUND THREAD **** + private void WorkerLoop() + { + var fileNameOnly = Path.GetFileName(_filePath); + + try + { + EnsureOnThread(); + + while (!_shutdownToken.IsCancellationRequested) + { + // Grab the parcel of work to do + var parcel = _main.GetParcel(); + if (parcel.Edits.Any()) + { + try + { + DocumentParseCompleteEventArgs args = null; + using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken)) + { + if (!linkedCancel.IsCancellationRequested) + { + // Collect ALL changes + List allEdits; + + if (_previouslyDiscarded != null) + { + allEdits = Enumerable.Concat(_previouslyDiscarded, parcel.Edits).ToList(); + } + else + { + allEdits = parcel.Edits.ToList(); + } + + var finalEdit = allEdits.Last(); + + var results = ParseChange(finalEdit.Snapshot, linkedCancel.Token); + + if (results != null && !linkedCancel.IsCancellationRequested) + { + // Clear discarded changes list + _previouslyDiscarded = null; + + var treeStructureChanged = _currentSyntaxTree == null || TreesAreDifferent(_currentSyntaxTree, results.GetSyntaxTree(), allEdits, parcel.CancelToken); + _currentSyntaxTree = results.GetSyntaxTree(); + + // Build Arguments + args = new DocumentParseCompleteEventArgs( + finalEdit.Change, + finalEdit.Snapshot, + treeStructureChanged, + results); + } + else + { + // Parse completed but we were cancelled in the mean time. Add these to the discarded changes set + _previouslyDiscarded = allEdits; + } + } + } + if (args != null) + { + _main.ReturnParcel(args); + } + } + catch (OperationCanceledException) + { + } + } + else + { + Thread.Yield(); + } + } + } + catch (OperationCanceledException) + { + // Do nothing. Just shut down. + } + finally + { + // Clean up main thread resources + _main.Dispose(); + } + } + + private RazorCodeDocument ParseChange(ITextSnapshot snapshot, CancellationToken token) + { + EnsureOnThread(); + + var sourceDocument = new TextSnapshotSourceDocument(snapshot, _filePath); + var imports = _templateEngine.GetImports(_filePath); + + var codeDocument = RazorCodeDocument.Create(sourceDocument, imports); + + _templateEngine.GenerateCode(codeDocument); + return codeDocument; + } + } + + private class WorkParcel + { + public WorkParcel(IList changes, CancellationToken cancelToken) + { + Edits = changes; + CancelToken = cancelToken; + } + + public CancellationToken CancelToken { get; } + + public IList Edits { get; } + } + + private class Edit + { + public Edit(SourceChange change, ITextSnapshot snapshot) + { + Change = change; + Snapshot = snapshot; + } + + public SourceChange Change { get; } + + public ITextSnapshot Snapshot { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DocumentParseCompleteEventArgs.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DocumentParseCompleteEventArgs.cs new file mode 100644 index 0000000000..c8dc5065c8 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DocumentParseCompleteEventArgs.cs @@ -0,0 +1,47 @@ +// 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.VisualStudio.Text; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + /// + /// Arguments for the event in . + /// + public sealed class DocumentParseCompleteEventArgs : EventArgs + { + public DocumentParseCompleteEventArgs( + SourceChange change, + ITextSnapshot buffer, + bool treeStructureChanged, + RazorCodeDocument codeDocument) + { + SourceChange = change; + Buffer = buffer; + TreeStructureChanged = treeStructureChanged; + CodeDocument = codeDocument; + } + + /// + /// The which triggered the re-parse. + /// + public SourceChange SourceChange { get; } + + /// + /// The text snapshot used in the re-parse. + /// + public ITextSnapshot Buffer { get; } + + /// + /// Indicates if the tree structure has actually changed since the previous re-parse. + /// + public bool TreeStructureChanged { get; } + + /// + /// The result of the parsing and code generation. + /// + public RazorCodeDocument CodeDocument { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/PartialParseResult.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/PartialParseResult.cs new file mode 100644 index 0000000000..2571342f69 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/PartialParseResult.cs @@ -0,0 +1,21 @@ +// 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; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + [Flags] + public enum PartialParseResult + { + Rejected = 1, + + Accepted = 2, + + Provisional = 4, + + SpanContextChanged = 8, + + AutoCompleteBlock = 16 + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorEditorParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorEditorParser.cs new file mode 100644 index 0000000000..7de8648a74 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorEditorParser.cs @@ -0,0 +1,187 @@ +// 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 System.Diagnostics; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + public class RazorEditorParser : IDisposable + { + private AspNetCore.Razor.Language.Legacy.Span _lastChangeOwner; + private AspNetCore.Razor.Language.Legacy.Span _lastAutoCompleteSpan; + private BackgroundParser _parser; + + public RazorEditorParser(RazorTemplateEngine templateEngine, string filePath) + { + if (templateEngine == null) + { + throw new ArgumentNullException(nameof(templateEngine)); + } + + if (string.IsNullOrEmpty(filePath)) + { + throw new ArgumentException( + AspNetCore.Razor.Language.Resources.ArgumentCannotBeNullOrEmpty, + nameof(filePath)); + } + + TemplateEngine = templateEngine; + _parser = new BackgroundParser(templateEngine, filePath); + _parser.ResultsReady += (sender, args) => OnDocumentParseComplete(args); + _parser.Start(); + } + + /// + /// Event fired when a full reparse of the document completes. + /// + public event EventHandler DocumentParseComplete; + + public RazorTemplateEngine TemplateEngine { get; } + + // Internal for testing. + internal RazorSyntaxTree CurrentSyntaxTree { get; private set; } + + // Internal for testing. + internal bool LastResultProvisional { get; private set; } + + public virtual string GetAutoCompleteString() + { + if (_lastAutoCompleteSpan?.EditHandler is AutoCompleteEditHandler editHandler) + { + return editHandler.AutoCompleteString; + } + + return null; + } + + public virtual PartialParseResult CheckForStructureChanges(SourceChange change, ITextSnapshot snapshot) + { + if (snapshot == null) + { + throw new ArgumentNullException(nameof(snapshot)); + } + + var result = PartialParseResultInternal.Rejected; + + using (_parser.SynchronizeMainThreadState()) + { + // Check if we can partial-parse + if (CurrentSyntaxTree != null && _parser.IsIdle) + { + result = TryPartialParse(change); + } + } + + // If partial parsing failed or there were outstanding parser tasks, start a full reparse + if ((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected) + { + _parser.QueueChange(change, snapshot); + } + + // Otherwise, remember if this was provisionally accepted for next partial parse + LastResultProvisional = (result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional; + VerifyFlagsAreValid(result); + + return (PartialParseResult)result; + } + + /// + /// Disposes of this parser. Should be called when the editor window is closed and the document is unloaded. + /// + public void Dispose() + { + _parser.Dispose(); + GC.SuppressFinalize(this); + } + + private PartialParseResultInternal TryPartialParse(SourceChange change) + { + var result = PartialParseResultInternal.Rejected; + + // Try the last change owner + if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change)) + { + var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change); + result = editResult.Result; + if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected) + { + _lastChangeOwner.ReplaceWith(editResult.EditedSpan); + } + + return result; + } + + // Locate the span responsible for this change + _lastChangeOwner = CurrentSyntaxTree.Root.LocateOwner(change); + + if (LastResultProvisional) + { + // Last change owner couldn't accept this, so we must do a full reparse + result = PartialParseResultInternal.Rejected; + } + else if (_lastChangeOwner != null) + { + var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change); + result = editResult.Result; + if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected) + { + _lastChangeOwner.ReplaceWith(editResult.EditedSpan); + } + if ((result & PartialParseResultInternal.AutoCompleteBlock) == PartialParseResultInternal.AutoCompleteBlock) + { + _lastAutoCompleteSpan = _lastChangeOwner; + } + else + { + _lastAutoCompleteSpan = null; + } + } + + return result; + } + + private void OnDocumentParseComplete(DocumentParseCompleteEventArgs args) + { + using (_parser.SynchronizeMainThreadState()) + { + CurrentSyntaxTree = args.CodeDocument.GetSyntaxTree(); + _lastChangeOwner = null; + } + + Debug.Assert(args != null, "Event arguments cannot be null"); + EventHandler handler = DocumentParseComplete; + if (handler != null) + { + try + { + handler(this, args); + } + catch (Exception ex) + { + Debug.WriteLine("[RzEd] Document Parse Complete Handler Threw: " + ex.ToString()); + } + } + } + + [Conditional("DEBUG")] + private static void VerifyFlagsAreValid(PartialParseResultInternal result) + { + Debug.Assert(((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted) || + ((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected), + "Partial Parse result does not have either of Accepted or Rejected flags set"); + Debug.Assert(((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected) || + ((result & PartialParseResultInternal.SpanContextChanged) != PartialParseResultInternal.SpanContextChanged), + "Partial Parse result was Accepted AND had SpanContextChanged flag set"); + Debug.Assert(((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected) || + ((result & PartialParseResultInternal.AutoCompleteBlock) != PartialParseResultInternal.AutoCompleteBlock), + "Partial Parse result was Accepted AND had AutoCompleteBlock flag set"); + Debug.Assert(((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted) || + ((result & PartialParseResultInternal.Provisional) != PartialParseResultInternal.Provisional), + "Partial Parse result was Rejected AND had Provisional flag set"); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/TextSnapshotSourceDocument.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/TextSnapshotSourceDocument.cs new file mode 100644 index 0000000000..37bb73f106 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/TextSnapshotSourceDocument.cs @@ -0,0 +1,79 @@ +// 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 System.Text; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + internal class TextSnapshotSourceDocument : RazorSourceDocument + { + private readonly ITextSnapshot _buffer; + private readonly RazorSourceLineCollection _lines; + + public TextSnapshotSourceDocument(ITextSnapshot snapshot, string filePath) + { + if (snapshot == null) + { + throw new ArgumentNullException(nameof(snapshot)); + } + + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + _buffer = snapshot; + FilePath = filePath; + + _lines = new DefaultRazorSourceLineCollection(this); + } + + public override char this[int position] => _buffer[position]; + + public override Encoding Encoding => Encoding.UTF8; + + public override int Length => _buffer.Length; + + public override RazorSourceLineCollection Lines => _lines; + + public override string FilePath { get; } + + public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) + { + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + if (sourceIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + + if (destinationIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(destinationIndex)); + } + + if (count < 0 || count > Length - sourceIndex || count > destination.Length - destinationIndex) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (count == 0) + { + return; + } + + for (var i = 0; i < count; i++) + { + destination[destinationIndex + i] = this[sourceIndex + i]; + } + } + + public override byte[] GetChecksum() => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/DirectiveTokenEditHandlerTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/DirectiveTokenEditHandlerTest.cs index 1a5aae94b7..0ddfa731e4 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/DirectiveTokenEditHandlerTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/DirectiveTokenEditHandlerTest.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Test var result = directiveTokenHandler.CanAcceptChange(target, sourceChange); // Assert - Assert.Equal(PartialParseResult.Accepted | PartialParseResult.Provisional, result); + Assert.Equal(PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional, result); } [Theory] @@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Test var result = directiveTokenHandler.CanAcceptChange(target, sourceChange); // Assert - Assert.Equal(PartialParseResult.Rejected, result); + Assert.Equal(PartialParseResultInternal.Rejected, result); } private class TestDirectiveTokenEditHandler : DirectiveTokenEditHandler @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Test { } - public new PartialParseResult CanAcceptChange(Span target, SourceChange change) + public new PartialParseResultInternal CanAcceptChange(Span target, SourceChange change) => base.CanAcceptChange(target, change); } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/StringTextBuffer.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/StringTextBuffer.cs deleted file mode 100644 index 3f5a94a859..0000000000 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/StringTextBuffer.cs +++ /dev/null @@ -1,53 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - public class StringTextBuffer : ITextBuffer, IDisposable - { - private string _buffer; - public bool Disposed { get; set; } - - public StringTextBuffer(string buffer) - { - _buffer = buffer; - } - - public int Length - { - get { return _buffer.Length; } - } - - public int Position { get; set; } - - public int Read() - { - if (Position >= _buffer.Length) - { - return -1; - } - return _buffer[Position++]; - } - - public int Peek() - { - if (Position >= _buffer.Length) - { - return -1; - } - return _buffer[Position]; - } - - public void Dispose() - { - Disposed = true; - } - - public object VersionToken - { - get { return _buffer; } - } - } -} diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.cshtml b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.cshtml deleted file mode 100644 index b50db22f77..0000000000 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.cshtml +++ /dev/null @@ -1,16 +0,0 @@ -@{ - string hello = "Hello, World"; -} - - - - Simple Page - - -

Simple Page

-

@hello

-

- @foreach(char c in hello) {@c} -

- - \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.txt deleted file mode 100644 index 69d42ccf0e..0000000000 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.txt +++ /dev/null @@ -1,43 +0,0 @@ -namespace Razor -{ - #line hidden - public class Template - { - #pragma warning disable 219 - private void __RazorDirectiveTokenHelpers__() { - } - #pragma warning restore 219 - private static System.Object __o = null; - #pragma warning disable 1998 - public async override global::System.Threading.Tasks.Task ExecuteAsync() - { -#line 1 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml" - - string hello = "Hello, World"; - -#line default -#line hidden -#line 11 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml" - __o = hello; - -#line default -#line hidden -#line 13 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml" - foreach(char c in hello) { - -#line default -#line hidden -#line 13 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml" - __o = c; - -#line default -#line hidden -#line 13 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml" - } - -#line default -#line hidden - } - #pragma warning restore 1998 - } -} diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockExtensions.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockExtensions.cs similarity index 100% rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockExtensions.cs rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockExtensions.cs diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockFactory.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs similarity index 100% rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockFactory.cs rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTypes.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs similarity index 100% rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTypes.cs rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/ErrorCollector.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ErrorCollector.cs similarity index 100% rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/ErrorCollector.cs rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ErrorCollector.cs diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/ParserTestBase.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs similarity index 100% rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/ParserTestBase.cs rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/RawTextSymbol.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/RawTextSymbol.cs similarity index 100% rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/RawTextSymbol.cs rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/RawTextSymbol.cs diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TestSpanBuilder.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/TestSpanBuilder.cs similarity index 100% rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TestSpanBuilder.cs rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/TestSpanBuilder.cs diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/SyntaxTreeVerifier.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs similarity index 100% rename from test/Microsoft.AspNetCore.Razor.Language.Test/SyntaxTreeVerifier.cs rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj index 96bf0959fb..1b1eb040d1 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj @@ -20,6 +20,7 @@ + diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorEditorParserTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorEditorParserTest.cs new file mode 100644 index 0000000000..5fd369d42a --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorEditorParserTest.cs @@ -0,0 +1,1479 @@ +// 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 System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.VisualStudio.Text; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + public class RazorEditorParserTest + { + private static readonly TestFile SimpleCSHTMLDocument = TestFile.Create("TestFiles/DesignTime/Simple.cshtml", typeof(RazorEditorParserTest)); + private static readonly TestFile SimpleCSHTMLDocumentGenerated = TestFile.Create("TestFiles/DesignTime/Simple.txt", typeof(RazorEditorParserTest)); + private const string TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml"; + + public static TheoryData TagHelperPartialParseRejectData + { + get + { + // change, (Block)expectedDocument + return new TheoryData + { + { + CreateInsertionChange("

", 2, " "), + new MarkupBlock( + new MarkupTagHelperBlock("p")) + }, + { + CreateInsertionChange("

", 6, " "), + new MarkupBlock( + new MarkupTagHelperBlock("p")) + }, + { + CreateInsertionChange("

", 12, " "), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "some-attr", + value: null, + attributeStructure: AttributeStructure.Minimized) + })) + }, + { + CreateInsertionChange("

", 12, "ibute"), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "some-attribute", + value: null, + attributeStructure: AttributeStructure.Minimized) + })) + }, + { + CreateInsertionChange("

", 2, " before"), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "before", + value: null, + attributeStructure: AttributeStructure.Minimized), + new TagHelperAttributeNode( + "some-attr", + value: null, + attributeStructure: AttributeStructure.Minimized) + })) + }, + }; + } + } + + [Theory] + [MemberData(nameof(TagHelperPartialParseRejectData))] + public void TagHelperTagBodiesRejectPartialChanges(object editObject, object expectedDocument) + { + // Arrange + var edit = (TestEdit)editObject; + var builder = TagHelperDescriptorBuilder.Create("PTagHelper", "TestAssembly"); + builder.SetTypeName("PTagHelper"); + builder.TagMatchingRule(rule => rule.TagName = "p"); + var descriptors = new[] + { + builder.Build() + }; + + var parser = new RazorEditorParser(CreateTemplateEngine(@"C:\This\Is\A\Test\Path"), @"C:\This\Is\A\Test\Path"); + + using (var manager = new TestParserManager(parser)) + { + manager.InitializeWithDocument(edit.OldSnapshot); + + // Act + var result = manager.CheckForStructureChangesAndWait(edit); + + // Assert + Assert.Equal(PartialParseResult.Rejected, result); + Assert.Equal(2, manager.ParseCount); + } + } + + public static TheoryData TagHelperAttributeAcceptData + { + get + { + var factory = new SpanFactory(); + + // change, (Block)expectedDocument, partialParseResult + return new TheoryData + { + { + CreateInsertionChange("

", 22, "."), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "str-attr", + new MarkupBlock( + new MarkupBlock( + new ExpressionBlock( + factory.CodeTransition(), + factory + .Code("DateTime.") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)))), + AttributeStructure.SingleQuotes) + })), + PartialParseResult.Accepted | PartialParseResult.Provisional + }, + { + CreateInsertionChange("

", 21, "."), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "obj-attr", + factory.CodeMarkup("DateTime."), + AttributeStructure.SingleQuotes) + })), + PartialParseResult.Accepted + }, + { + CreateInsertionChange("

", 25, "."), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "obj-attr", + factory.CodeMarkup("1 + DateTime."), + AttributeStructure.SingleQuotes) + })), + PartialParseResult.Accepted + }, + { + CreateInsertionChange("

", 34, "."), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "before-attr", + value: null, + attributeStructure: AttributeStructure.Minimized), + new TagHelperAttributeNode( + "str-attr", + new MarkupBlock( + new MarkupBlock( + new ExpressionBlock( + factory.CodeTransition(), + factory + .Code("DateTime.") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)))), + AttributeStructure.SingleQuotes), + new TagHelperAttributeNode( + "after-attr", + value: null, + attributeStructure: AttributeStructure.Minimized), + })), + PartialParseResult.Accepted | PartialParseResult.Provisional + }, + { + CreateInsertionChange("

", 29, "."), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "str-attr", + new MarkupBlock( + factory.Markup("before"), + new MarkupBlock( + factory.Markup(" "), + new ExpressionBlock( + factory.CodeTransition(), + factory + .Code("DateTime.") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace))), + factory.Markup(" after")), + AttributeStructure.SingleQuotes) + })), + PartialParseResult.Accepted | PartialParseResult.Provisional + }, + }; + } + } + + [Theory] + [MemberData(nameof(TagHelperAttributeAcceptData))] + public void TagHelperAttributesAreLocatedAndAcceptChangesCorrectly( + object editObject, + object expectedDocument, + PartialParseResult partialParseResult) + { + // Arrange + var edit = (TestEdit)editObject; + var builder = TagHelperDescriptorBuilder.Create("PTagHelper", "Test"); + builder.SetTypeName("PTagHelper"); + builder.TagMatchingRule(rule => rule.TagName = "p"); + builder.BindAttribute(attribute => + { + attribute.Name = "obj-attr"; + attribute.TypeName = typeof(object).FullName; + attribute.SetPropertyName("ObjectAttribute"); + }); + builder.BindAttribute(attribute => + { + attribute.Name = "str-attr"; + attribute.TypeName = typeof(string).FullName; + attribute.SetPropertyName("StringAttribute"); + }); + var descriptors = new[] { builder.Build() }; + + var parser = new RazorEditorParser(CreateTemplateEngine(@"C:\This\Is\A\Test\Path", descriptors), @"C:\This\Is\A\Test\Path"); + + using (var manager = new TestParserManager(parser)) + { + manager.InitializeWithDocument(edit.OldSnapshot); + + // Act + var result = manager.CheckForStructureChangesAndWait(edit); + + // Assert + Assert.Equal(partialParseResult, result); + Assert.Equal(1, manager.ParseCount); + } + } + + [Fact] + public void ConstructorRequiresNonNullPhysicalPath() + { + Assert.Throws("filePath", () => new RazorEditorParser(CreateTemplateEngine(), null)); + } + + [Fact] + public void ConstructorRequiresNonEmptyPhysicalPath() + { + Assert.Throws("filePath", () => new RazorEditorParser(CreateTemplateEngine(), string.Empty)); + } + + [Theory] + [InlineData(" ")] + [InlineData("\r\n")] + [InlineData("abcdefg")] + [InlineData("\f\r\n abcd \t")] + public void TreesAreDifferentReturnsFalseForAddedContent(string content) + { + // Arrange + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var original = new MarkupBlock( + blockFactory.MarkupTagBlock("

"), + blockFactory.TagHelperBlock( + tagName: "div", + tagMode: TagMode.StartTagAndEndTag, + start: new SourceLocation(3, 0, 3), + startTag: blockFactory.MarkupTagBlock("

"), + children: new SyntaxTreeNode[] + { + factory.Markup($"{Environment.NewLine}{Environment.NewLine}") + }, + endTag: blockFactory.MarkupTagBlock("
")), + blockFactory.MarkupTagBlock("

")); + + factory.Reset(); + + var modified = new MarkupBlock( + blockFactory.MarkupTagBlock("

"), + blockFactory.TagHelperBlock( + tagName: "div", + tagMode: TagMode.StartTagAndEndTag, + start: new SourceLocation(3, 0, 3), + startTag: blockFactory.MarkupTagBlock("

"), + children: new SyntaxTreeNode[] + { + factory.Markup($"{Environment.NewLine}{content}{Environment.NewLine}") + }, + endTag: blockFactory.MarkupTagBlock("
")), + blockFactory.MarkupTagBlock("

")); + original.LinkNodes(); + modified.LinkNodes(); + + // Act + var treesAreDifferent = BackgroundParser.TreesAreDifferent( + original, + modified, + new[] + { + new SourceChange( + absoluteIndex: 8 + Environment.NewLine.Length, + length: 0, + newText: content) + }, + CancellationToken.None); + + // Assert + Assert.False(treesAreDifferent); + } + + [Fact] + public void TreesAreDifferentReturnsTrueIfTreeStructureIsDifferent() + { + var factory = new SpanFactory(); + var original = new MarkupBlock( + factory.Markup("

"), + new ExpressionBlock( + factory.CodeTransition()), + factory.Markup("

")); + var modified = new MarkupBlock( + factory.Markup("

"), + new ExpressionBlock( + factory.CodeTransition("@"), + factory.Code("f") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)), + factory.Markup("

")); + Assert.True(BackgroundParser.TreesAreDifferent( + original, + modified, + new[] + { + new SourceChange(absoluteIndex: 4, length: 0, newText: "f") + }, + CancellationToken.None)); + } + + [Fact] + public void TreesAreDifferentReturnsFalseIfTreeStructureIsSame() + { + var factory = new SpanFactory(); + var original = new MarkupBlock( + factory.Markup("

"), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("f") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)), + factory.Markup("

")); + factory.Reset(); + var modified = new MarkupBlock( + factory.Markup("

"), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("foo") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)), + factory.Markup("

")); + original.LinkNodes(); + modified.LinkNodes(); + Assert.False(BackgroundParser.TreesAreDifferent( + original, + modified, + new[] + { + new SourceChange(absoluteIndex: 5, length: 0, newText: "oo") + }, + CancellationToken.None)); + } + + [Fact] + public void CheckForStructureChangesStartsFullReparseIfChangeOverlapsMultipleSpans() + { + // Arrange + using (var parser = new RazorEditorParser(CreateTemplateEngine(), TestLinePragmaFileName)) + { + var original = new StringTextSnapshot("Foo @bar Baz"); + var changed = new StringTextSnapshot("Foo @bap Daz"); + var change = new SourceChange(7, 3, "p D"); + + var parseComplete = new ManualResetEventSlim(); + var parseCount = 0; + parser.DocumentParseComplete += (sender, args) => + { + Interlocked.Increment(ref parseCount); + parseComplete.Set(); + }; + + Assert.Equal(PartialParseResult.Rejected, parser.CheckForStructureChanges(change, original)); + DoWithTimeoutIfNotDebugging(parseComplete.Wait); // Wait for the parse to finish + parseComplete.Reset(); + + // Act + var result = parser.CheckForStructureChanges(change, original); + + // Assert + Assert.Equal(PartialParseResult.Rejected, result); + DoWithTimeoutIfNotDebugging(parseComplete.Wait); + Assert.Equal(2, parseCount); + } + } + + [Fact] + public void AwaitPeriodInsertionAcceptedProvisionally() + { + // Arrange + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("foo @await Html. baz"); + var old = new StringTextSnapshot("foo @await Html baz"); + + // Act and Assert + RunPartialParseTest(new TestEdit(15, 0, old, 1, changed, "."), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("await Html.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.WhiteSpace | AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz")), additionalFlags: PartialParseResult.Provisional); + } + + [Fact] + public void ImplicitExpressionAcceptsInnerInsertionsInStatementBlock() + { + // Arrange + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("@{" + Environment.NewLine + + " @DateTime..Now" + Environment.NewLine + + "}"); + var old = new StringTextSnapshot("@{" + Environment.NewLine + + " @DateTime.Now" + Environment.NewLine + + "}"); + + // Act and Assert + RunPartialParseTest(new TestEdit(17, 0, old, 1, changed, "."), + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), + factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime..Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Code(Environment.NewLine).AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), + factory.EmptyHtml())); + } + + [Fact] + public void ImplicitExpressionAcceptsInnerInsertions() + { + // Arrange + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("foo @DateTime..Now baz"); + var old = new StringTextSnapshot("foo @DateTime.Now baz"); + + // Act and Assert + RunPartialParseTest(new TestEdit(13, 0, old, 1, changed, "."), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime..Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz")), additionalFlags: PartialParseResult.Provisional); + } + + [Fact] + public void ImplicitExpressionAcceptsWholeIdentifierReplacement() + { + // Arrange + var factory = new SpanFactory(); + var old = new StringTextSnapshot("foo @date baz"); + var changed = new StringTextSnapshot("foo @DateTime baz"); + + // Act and Assert + RunPartialParseTest(new TestEdit(5, 4, old, 8, changed, "DateTime"), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionRejectsWholeIdentifierReplacementToKeyword() + { + // Arrange + var parser = new RazorEditorParser(CreateTemplateEngine(@"C:\This\Is\A\Test\Path"), @"C:\This\Is\A\Test\Path"); + + using (var manager = new TestParserManager(parser)) + { + var old = new StringTextSnapshot("foo @date baz"); + var changed = new StringTextSnapshot("foo @if baz"); + var edit = new TestEdit(5, 4, old, 2, changed, "if"); + manager.InitializeWithDocument(old); + + // Act + var result = manager.CheckForStructureChangesAndWait(edit); + + // Assert + Assert.Equal(PartialParseResult.Rejected, result); + Assert.Equal(2, manager.ParseCount); + } + } + + [Fact] + public void ImplicitExpressionRejectsWholeIdentifierReplacementToDirective() + { + // Arrange + var parser = new RazorEditorParser(CreateTemplateEngine(@"C:\This\Is\A\Test\Path"), @"C:\This\Is\A\Test\Path"); + + using (var manager = new TestParserManager(parser)) + { + var old = new StringTextSnapshot("foo @date baz"); + var changed = new StringTextSnapshot("foo @inherits baz"); + var SourceChange = new TestEdit(5, 4, old, 8, changed, "inherits"); + manager.InitializeWithDocument(old); + + // Act + var result = manager.CheckForStructureChangesAndWait(SourceChange); + + // Assert + Assert.Equal(PartialParseResult.Rejected | PartialParseResult.SpanContextChanged, result); + Assert.Equal(2, manager.ParseCount); + } + } + + [Fact] + public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_SingleSymbol() + { + // Arrange + var factory = new SpanFactory(); + var old = new StringTextSnapshot("foo @dTime baz"); + var changed = new StringTextSnapshot("foo @DateTime baz"); + + // Act and Assert + RunPartialParseTest(new TestEdit(5, 1, old, 4, changed, "Date"), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_MultipleSymbols() + { + // Arrange + var factory = new SpanFactory(); + var old = new StringTextSnapshot("foo @dTime.Now baz"); + var changed = new StringTextSnapshot("foo @DateTime.Now baz"); + + // Act and Assert + RunPartialParseTest(new TestEdit(5, 1, old, 4, changed, "Date"), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_SingleSymbol() + { + // Arrange + var factory = new SpanFactory(); + var old = new StringTextSnapshot("foo @Datet baz"); + var changed = new StringTextSnapshot("foo @DateTime baz"); + + // Act and Assert + RunPartialParseTest(new TestEdit(9, 1, old, 4, changed, "Time"), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_MultipleSymbols() + { + // Arrange + var factory = new SpanFactory(); + var old = new StringTextSnapshot("foo @DateTime.n baz"); + var changed = new StringTextSnapshot("foo @DateTime.Now baz"); + + // Act and Assert + RunPartialParseTest(new TestEdit(14, 1, old, 3, changed, "Now"), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionAcceptsSurroundedIdentifierReplacements() + { + // Arrange + var factory = new SpanFactory(); + var old = new StringTextSnapshot("foo @DateTime.n.ToString() baz"); + var changed = new StringTextSnapshot("foo @DateTime.Now.ToString() baz"); + + // Act and Assert + RunPartialParseTest(new TestEdit(14, 1, old, 3, changed, "Now"), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now.ToString()").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlockAfterIdentifiers() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("@{" + Environment.NewLine + + " @DateTime." + Environment.NewLine + + "}"); + var old = new StringTextSnapshot("@{" + Environment.NewLine + + " @DateTime" + Environment.NewLine + + "}"); + + var edit = new TestEdit(15 + Environment.NewLine.Length, 0, old, 1, changed, "."); + using (var manager = CreateParserManager()) + { + Action applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) => + { + var result = manager.CheckForStructureChangesAndWait(edit); + + // Assert + Assert.Equal(expectedResult, result); + Assert.Equal(1, manager.ParseCount); + ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), + factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code(expectedCode) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Code(Environment.NewLine).AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), + factory.EmptyHtml())); + }; + + manager.InitializeWithDocument(edit.OldSnapshot); + + // This is the process of a dotless commit when doing "." insertions to commit intellisense changes. + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateTime."); + + old = changed; + changed = new StringTextSnapshot("@{" + Environment.NewLine + + " @DateTime.." + Environment.NewLine + + "}"); + edit = new TestEdit(16 + Environment.NewLine.Length, 0, old, 1, changed, "."); + + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateTime.."); + + old = changed; + changed = new StringTextSnapshot("@{" + Environment.NewLine + + " @DateTime.Now." + Environment.NewLine + + "}"); + edit = new TestEdit(16 + Environment.NewLine.Length, 0, old, 3, changed, "Now"); + + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateTime.Now."); + } + } + + [Fact] + public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlock() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("@{" + Environment.NewLine + + " @DateT." + Environment.NewLine + + "}"); + var old = new StringTextSnapshot("@{" + Environment.NewLine + + " @DateT" + Environment.NewLine + + "}"); + + var edit = new TestEdit(12 + Environment.NewLine.Length, 0, old, 1, changed, "."); + using (var manager = CreateParserManager()) + { + Action applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) => + { + var result = manager.CheckForStructureChangesAndWait(edit); + + // Assert + Assert.Equal(expectedResult, result); + Assert.Equal(1, manager.ParseCount); + ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), + factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code(expectedCode) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Code(Environment.NewLine).AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), + factory.EmptyHtml())); + }; + + manager.InitializeWithDocument(edit.OldSnapshot); + + // This is the process of a dotless commit when doing "." insertions to commit intellisense changes. + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateT."); + + old = changed; + changed = new StringTextSnapshot("@{" + Environment.NewLine + + " @DateTime." + Environment.NewLine + + "}"); + edit = new TestEdit(12 + Environment.NewLine.Length, 0, old, 3, changed, "ime"); + + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateTime."); + } + } + + [Fact] + public void ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertions() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("foo @DateT. baz"); + var old = new StringTextSnapshot("foo @DateT baz"); + var edit = new TestEdit(10, 0, old, 1, changed, "."); + using (var manager = CreateParserManager()) + { + Action applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) => + { + var result = manager.CheckForStructureChangesAndWait(edit); + + // Assert + Assert.Equal(expectedResult, result); + Assert.Equal(1, manager.ParseCount); + + ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code(expectedCode).AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz"))); + }; + + manager.InitializeWithDocument(edit.OldSnapshot); + + // This is the process of a dotless commit when doing "." insertions to commit intellisense changes. + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateT."); + + old = changed; + changed = new StringTextSnapshot("foo @DateTime. baz"); + edit = new TestEdit(10, 0, old, 3, changed, "ime"); + + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime."); + } + } + + [Fact] + public void ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertionsAfterIdentifiers() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("foo @DateTime. baz"); + var old = new StringTextSnapshot("foo @DateTime baz"); + var edit = new TestEdit(13, 0, old, 1, changed, "."); + using (var manager = CreateParserManager()) + { + Action applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) => + { + var result = manager.CheckForStructureChangesAndWait(edit); + + // Assert + Assert.Equal(expectedResult, result); + Assert.Equal(1, manager.ParseCount); + + ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code(expectedCode).AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz"))); + }; + + manager.InitializeWithDocument(edit.OldSnapshot); + + // This is the process of a dotless commit when doing "." insertions to commit intellisense changes. + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime."); + + old = changed; + changed = new StringTextSnapshot("foo @DateTime.. baz"); + edit = new TestEdit(14, 0, old, 1, changed, "."); + + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime.."); + + old = changed; + changed = new StringTextSnapshot("foo @DateTime.Now. baz"); + edit = new TestEdit(14, 0, old, 3, changed, "Now"); + + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime.Now."); + } + } + + [Fact] + public void ImplicitExpressionProvisionallyAcceptsCaseInsensitiveDotlessCommitInsertions_NewRoslynIntegration() + { + var factory = new SpanFactory(); + var old = new StringTextSnapshot("foo @date baz"); + var changed = new StringTextSnapshot("foo @date. baz"); + var edit = new TestEdit(9, 0, old, 1, changed, "."); + using (var manager = CreateParserManager()) + { + Action applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) => + { + var result = manager.CheckForStructureChangesAndWait(edit); + + // Assert + Assert.Equal(expectedResult, result); + Assert.Equal(1, manager.ParseCount); + + ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code(expectedCode).AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz"))); + }; + + manager.InitializeWithDocument(edit.OldSnapshot); + + // This is the process of a dotless commit when doing "." insertions to commit intellisense changes. + + // @date => @date. + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "date."); + + old = changed; + changed = new StringTextSnapshot("foo @date baz"); + edit = new TestEdit(9, 1, old, 0, changed, ""); + + // @date. => @date + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "date"); + + old = changed; + changed = new StringTextSnapshot("foo @DateTime baz"); + edit = new TestEdit(5, 4, old, 8, changed, "DateTime"); + + // @date => @DateTime + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateTime"); + + old = changed; + changed = new StringTextSnapshot("foo @DateTime. baz"); + edit = new TestEdit(13, 0, old, 1, changed, "."); + + // @DateTime => @DateTime. + applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime."); + } + } + + [Fact] + public void ImplicitExpressionProvisionallyAcceptsDeleteOfIdentifierPartsIfDotRemains() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("foo @User. baz"); + var old = new StringTextSnapshot("foo @User.Name baz"); + RunPartialParseTest(new TestEdit(10, 4, old, 0, changed, string.Empty), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz")), + additionalFlags: PartialParseResult.Provisional); + } + + [Fact] + public void ImplicitExpressionAcceptsDeleteOfIdentifierPartsIfSomeOfIdentifierRemains() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("foo @Us baz"); + var old = new StringTextSnapshot("foo @User baz"); + RunPartialParseTest(new TestEdit(7, 2, old, 0, changed, string.Empty), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("Us").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionProvisionallyAcceptsMultipleInsertionIfItCausesIdentifierExpansionAndTrailingDot() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("foo @User. baz"); + var old = new StringTextSnapshot("foo @U baz"); + RunPartialParseTest(new TestEdit(6, 0, old, 4, changed, "ser."), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz")), + additionalFlags: PartialParseResult.Provisional); + } + + [Fact] + public void ImplicitExpressionAcceptsMultipleInsertionIfItOnlyCausesIdentifierExpansion() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("foo @barbiz baz"); + var old = new StringTextSnapshot("foo @bar baz"); + RunPartialParseTest(new TestEdit(8, 0, old, 3, changed, "biz"), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("barbiz").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionAcceptsIdentifierExpansionAtEndOfNonWhitespaceCharacters() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("@{" + Environment.NewLine + + " @food" + Environment.NewLine + + "}"); + var old = new StringTextSnapshot("@{" + Environment.NewLine + + " @foo" + Environment.NewLine + + "}"); + RunPartialParseTest(new TestEdit(10 + Environment.NewLine.Length, 0, old, 1, changed, "d"), + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), + factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("food") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Code(Environment.NewLine).AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), + factory.EmptyHtml())); + } + + [Fact] + public void ImplicitExpressionAcceptsIdentifierAfterDotAtEndOfNonWhitespaceCharacters() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("@{" + Environment.NewLine + + " @foo.d" + Environment.NewLine + + "}"); + var old = new StringTextSnapshot("@{" + Environment.NewLine + + " @foo." + Environment.NewLine + + "}"); + RunPartialParseTest(new TestEdit(11 + Environment.NewLine.Length, 0, old, 1, changed, "d"), + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), + factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("foo.d") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Code(Environment.NewLine).AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), + factory.EmptyHtml())); + } + + [Fact] + public void ImplicitExpressionAcceptsDotAtEndOfNonWhitespaceCharacters() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("@{" + Environment.NewLine + + " @foo." + Environment.NewLine + + "}"); + var old = new StringTextSnapshot("@{" + Environment.NewLine + + " @foo" + Environment.NewLine + + "}"); + RunPartialParseTest(new TestEdit(10 + Environment.NewLine.Length, 0, old, 1, changed, "."), + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), + factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code(@"foo.") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Code(Environment.NewLine).AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), + factory.EmptyHtml())); + } + + [Fact] + public void ImplicitExpressionRejectsChangeWhichWouldHaveBeenAcceptedIfLastChangeWasProvisionallyAcceptedOnDifferentSpan() + { + var factory = new SpanFactory(); + + // Arrange + var dotTyped = new TestEdit(8, 0, new StringTextSnapshot("foo @foo @bar"), 1, new StringTextSnapshot("foo @foo. @bar"), "."); + var charTyped = new TestEdit(14, 0, new StringTextSnapshot("foo @foo. @bar"), 1, new StringTextSnapshot("foo @foo. @barb"), "b"); + using (var manager = CreateParserManager()) + { + manager.InitializeWithDocument(dotTyped.OldSnapshot); + + // Apply the dot change + Assert.Equal(PartialParseResult.Provisional | PartialParseResult.Accepted, manager.CheckForStructureChangesAndWait(dotTyped)); + + // Act (apply the identifier start char change) + var result = manager.CheckForStructureChangesAndWait(charTyped); + + // Assert + Assert.Equal(PartialParseResult.Rejected, result); + Assert.False(manager.Parser.LastResultProvisional, "LastResultProvisional flag should have been cleared but it was not"); + ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("foo") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(". "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("barb") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.EmptyHtml())); + } + } + + [Fact] + public void ImplicitExpressionAcceptsIdentifierTypedAfterDotIfLastChangeWasProvisionalAcceptanceOfDot() + { + var factory = new SpanFactory(); + + // Arrange + var dotTyped = new TestEdit(8, 0, new StringTextSnapshot("foo @foo bar"), 1, new StringTextSnapshot("foo @foo. bar"), "."); + var charTyped = new TestEdit(9, 0, new StringTextSnapshot("foo @foo. bar"), 1, new StringTextSnapshot("foo @foo.b bar"), "b"); + using (var manager = CreateParserManager()) + { + manager.InitializeWithDocument(dotTyped.OldSnapshot); + + // Apply the dot change + Assert.Equal(PartialParseResult.Provisional | PartialParseResult.Accepted, manager.CheckForStructureChangesAndWait(dotTyped)); + + // Act (apply the identifier start char change) + var result = manager.CheckForStructureChangesAndWait(charTyped); + + // Assert + Assert.Equal(PartialParseResult.Accepted, result); + Assert.False(manager.Parser.LastResultProvisional, "LastResultProvisional flag should have been cleared but it was not"); + ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("foo.b") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" bar"))); + } + } + + [Fact] + public void ImplicitExpressionProvisionallyAcceptsDotAfterIdentifierInMarkup() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("foo @foo. bar"); + var old = new StringTextSnapshot("foo @foo bar"); + RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "."), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("foo.") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" bar")), + additionalFlags: PartialParseResult.Provisional); + } + + [Fact] + public void ImplicitExpressionAcceptsAdditionalIdentifierCharactersIfEndOfSpanIsIdentifier() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("foo @foob bar"); + var old = new StringTextSnapshot("foo @foo bar"); + RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "b"), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("foob") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" bar"))); + } + + [Fact] + public void ImplicitExpressionAcceptsAdditionalIdentifierStartCharactersIfEndOfSpanIsDot() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("@{@foo.b}"); + var old = new StringTextSnapshot("@{@foo.}"); + RunPartialParseTest(new TestEdit(7, 0, old, 1, changed, "b"), + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), + factory.EmptyCSharp() + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("foo.b") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), + factory.EmptyHtml())); + } + + [Fact] + public void ImplicitExpressionAcceptsDotIfTrailingDotsAreAllowed() + { + var factory = new SpanFactory(); + var changed = new StringTextSnapshot("@{@foo.}"); + var old = new StringTextSnapshot("@{@foo}"); + RunPartialParseTest(new TestEdit(6, 0, old, 1, changed, "."), + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), + factory.EmptyCSharp() + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("foo.") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), + factory.EmptyHtml())); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfIfKeywordTyped() + { + RunTypeKeywordTest("if"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfDoKeywordTyped() + { + RunTypeKeywordTest("do"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfTryKeywordTyped() + { + RunTypeKeywordTest("try"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfForKeywordTyped() + { + RunTypeKeywordTest("for"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfForEachKeywordTyped() + { + RunTypeKeywordTest("foreach"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfWhileKeywordTyped() + { + RunTypeKeywordTest("while"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfSwitchKeywordTyped() + { + RunTypeKeywordTest("switch"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfLockKeywordTyped() + { + RunTypeKeywordTest("lock"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfUsingKeywordTyped() + { + RunTypeKeywordTest("using"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfSectionKeywordTyped() + { + RunTypeKeywordTest("section"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfInheritsKeywordTyped() + { + RunTypeKeywordTest("inherits"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfFunctionsKeywordTyped() + { + RunTypeKeywordTest("functions"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfNamespaceKeywordTyped() + { + RunTypeKeywordTest("namespace"); + } + + [Fact] + public void ImplicitExpressionCorrectlyTriggersReparseIfClassKeywordTyped() + { + RunTypeKeywordTest("class"); + } + + private static TestEdit CreateInsertionChange(string initialText, int insertionLocation, string insertionText) + { + var changedText = initialText.Insert(insertionLocation, insertionText); + var sourceChange = new SourceChange(insertionLocation, 0, insertionText); + var oldSnapshot = new StringTextSnapshot(initialText); + var changedSnapshot = new StringTextSnapshot(changedText); + return new TestEdit + { + Change = sourceChange, + OldSnapshot = oldSnapshot, + NewSnapshot = changedSnapshot, + }; + } + + private static void RunFullReparseTest(TestEdit edit, PartialParseResult additionalFlags = (PartialParseResult)0) + { + // Arrange + using (var manager = CreateParserManager()) + { + manager.InitializeWithDocument(edit.OldSnapshot); + + // Act + var result = manager.CheckForStructureChangesAndWait(edit); + + // Assert + Assert.Equal(PartialParseResult.Rejected | additionalFlags, result); + Assert.Equal(2, manager.ParseCount); + } + } + + private static void RunPartialParseTest(TestEdit edit, Block newTreeRoot, PartialParseResult additionalFlags = (PartialParseResult)0) + { + // Arrange + using (var manager = CreateParserManager()) + { + manager.InitializeWithDocument(edit.OldSnapshot); + + // Act + var result = manager.CheckForStructureChangesAndWait(edit); + + // Assert + Assert.Equal(PartialParseResult.Accepted | additionalFlags, result); + Assert.Equal(1, manager.ParseCount); + ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, newTreeRoot); + } + } + + private static TestParserManager CreateParserManager() + { + var parser = new RazorEditorParser(CreateTemplateEngine(), TestLinePragmaFileName); + return new TestParserManager(parser); + } + + private static RazorTemplateEngine CreateTemplateEngine( + string path = TestLinePragmaFileName, + IEnumerable tagHelpers = null) + { + var engine = RazorEngine.CreateDesignTime(builder => + { + RazorExtensions.Register(builder); + + if (tagHelpers != null) + { + builder.AddTagHelpers(tagHelpers); + } + }); + + // GetImports on RazorTemplateEngine will at least check that the item exists, so we need to pretend + // that it does. + var items = new List(); + items.Add(new TestRazorProjectItem(path)); + + var project = new TestRazorProject(items); + + var templateEngine = new RazorTemplateEngine(engine, project); + templateEngine.Options.DefaultImports = RazorSourceDocument.Create("@addTagHelper *, Test", "_TestImports.cshtml"); + return templateEngine; + } + + private static void RunTypeKeywordTest(string keyword) + { + var before = "@" + keyword.Substring(0, keyword.Length - 1); + var after = "@" + keyword; + var changed = new StringTextSnapshot(after); + var old = new StringTextSnapshot(before); + var change = new SourceChange(keyword.Length, 0, keyword[keyword.Length - 1].ToString()); + var edit = new TestEdit + { + Change = change, + NewSnapshot = changed, + OldSnapshot = old + }; + RunFullReparseTest(edit, additionalFlags: PartialParseResult.SpanContextChanged); + } + + private static void DoWithTimeoutIfNotDebugging(Func withTimeout) + { +#if DEBUG + if (Debugger.IsAttached) + { + withTimeout(Timeout.Infinite); + } + else + { +#endif + Assert.True(withTimeout((int)TimeSpan.FromSeconds(1).TotalMilliseconds), "Timeout expired!"); +#if DEBUG + } +#endif + } + + private class TestParserManager : IDisposable + { + public int ParseCount; + + private readonly ManualResetEventSlim _parserComplete; + + public TestParserManager(RazorEditorParser parser) + { + _parserComplete = new ManualResetEventSlim(); + ParseCount = 0; + Parser = parser; + parser.DocumentParseComplete += (sender, args) => + { + Interlocked.Increment(ref ParseCount); + _parserComplete.Set(); + }; + } + + public RazorEditorParser Parser { get; } + + public void InitializeWithDocument(ITextSnapshot snapshot) + { + var initialChange = new SourceChange(0, 0, string.Empty); + var edit = new TestEdit + { + Change = initialChange, + OldSnapshot = snapshot, + NewSnapshot = snapshot + }; + CheckForStructureChangesAndWait(edit); + } + + public PartialParseResult CheckForStructureChangesAndWait(TestEdit edit) + { + var result = Parser.CheckForStructureChanges(edit.Change, edit.NewSnapshot); + if (result.HasFlag(PartialParseResult.Rejected)) + { + WaitForParse(); + } + return result; + } + + public void WaitForParse() + { + DoWithTimeoutIfNotDebugging(_parserComplete.Wait); // Wait for the parse to finish + _parserComplete.Reset(); + } + + public void Dispose() + { + Parser.Dispose(); + } + } + + private class TestEdit + { + public TestEdit() + { + } + + public TestEdit(int position, int oldLength, ITextSnapshot oldSnapshot, int newLength, ITextSnapshot newSnapshot, string newText) + { + Change = new SourceChange(position, oldLength, newText); + OldSnapshot = oldSnapshot; + NewSnapshot = newSnapshot; + } + + public SourceChange Change { get; set; } + + public ITextSnapshot OldSnapshot { get; set; } + + public ITextSnapshot NewSnapshot { get; set; } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/StringTextSnapshot.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/StringTextSnapshot.cs new file mode 100644 index 0000000000..c6e183029b --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/StringTextSnapshot.cs @@ -0,0 +1,106 @@ +// 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 System.Collections.Generic; +using System.IO; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + public class StringTextSnapshot : ITextSnapshot + { + private readonly string _content; + + public StringTextSnapshot(string content) + { + _content = content; + } + + public char this[int position] => _content[position]; + + public VisualStudio.Text.ITextBuffer TextBuffer => throw new NotImplementedException(); + + public IContentType ContentType => throw new NotImplementedException(); + + public ITextVersion Version => throw new NotImplementedException(); + + public int Length => _content.Length; + + public int LineCount => throw new NotImplementedException(); + + public IEnumerable Lines => throw new NotImplementedException(); + + public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) + { + _content.CopyTo(sourceIndex, destination, destinationIndex, count); + } + + public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode) + { + throw new NotImplementedException(); + } + + public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) + { + throw new NotImplementedException(); + } + + public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode) + { + throw new NotImplementedException(); + } + + public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) + { + throw new NotImplementedException(); + } + + public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode) + { + throw new NotImplementedException(); + } + + public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) + { + throw new NotImplementedException(); + } + + public ITextSnapshotLine GetLineFromLineNumber(int lineNumber) + { + throw new NotImplementedException(); + } + + public ITextSnapshotLine GetLineFromPosition(int position) + { + throw new NotImplementedException(); + } + + public int GetLineNumberFromPosition(int position) + { + throw new NotImplementedException(); + } + + public string GetText(VisualStudio.Text.Span span) + { + throw new NotImplementedException(); + } + + public string GetText(int startIndex, int length) => _content.Substring(startIndex, length); + + public string GetText() => _content; + + public char[] ToCharArray(int startIndex, int length) => _content.ToCharArray(); + + public void Write(TextWriter writer, VisualStudio.Text.Span span) + { + throw new NotImplementedException(); + } + + public void Write(TextWriter writer) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestBoundAttributeDescriptorBuilderExtensions.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestBoundAttributeDescriptorBuilderExtensions.cs similarity index 97% rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestBoundAttributeDescriptorBuilderExtensions.cs rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestBoundAttributeDescriptorBuilderExtensions.cs index 23987d03bf..d784d7b1c0 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestBoundAttributeDescriptorBuilderExtensions.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestBoundAttributeDescriptorBuilderExtensions.cs @@ -2,8 +2,9 @@ // 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; -namespace Microsoft.AspNetCore.Razor.Language +namespace Microsoft.VisualStudio.LanguageServices.Razor { public static class TestBoundAttributeDescriptorBuilderExtensions { diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestRequiredAttributeDescriptorBuilderExtensions.cs similarity index 95% rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestRequiredAttributeDescriptorBuilderExtensions.cs index da638e1750..16c4ea9100 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestRequiredAttributeDescriptorBuilderExtensions.cs @@ -2,8 +2,9 @@ // 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; -namespace Microsoft.AspNetCore.Razor.Language +namespace Microsoft.VisualStudio.LanguageServices.Razor { public static class TestRequiredAttributeDescriptorBuilderExtensions { diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagHelperDescriptorBuilderExtensions.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagHelperDescriptorBuilderExtensions.cs similarity index 97% rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagHelperDescriptorBuilderExtensions.cs rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagHelperDescriptorBuilderExtensions.cs index 5bc81a7792..c32a61580e 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagHelperDescriptorBuilderExtensions.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagHelperDescriptorBuilderExtensions.cs @@ -2,8 +2,9 @@ // 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; -namespace Microsoft.AspNetCore.Razor.Language +namespace Microsoft.VisualStudio.LanguageServices.Razor { public static class TestTagHelperDescriptorBuilderExtensions { diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagMatchingRuleDescriptorBuilderExtensions.cs similarity index 95% rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagMatchingRuleDescriptorBuilderExtensions.cs index f052818fe6..5373498774 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagMatchingRuleDescriptorBuilderExtensions.cs @@ -2,8 +2,9 @@ // 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; -namespace Microsoft.AspNetCore.Razor.Language +namespace Microsoft.VisualStudio.LanguageServices.Razor { public static class TestTagMatchingRuleDescriptorBuilderExtensions {