Port the legacy RazorEditorParser
This commit is contained in:
parent
9b3e8d0cda
commit
90b48347a5
|
|
@ -0,0 +1,424 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||
{
|
||||
internal class BackgroundParser : IDisposable
|
||||
{
|
||||
private MainThreadState _main;
|
||||
private BackgroundThread _bg;
|
||||
|
||||
public BackgroundParser(RazorTemplateEngine templateEngine, string fileName)
|
||||
{
|
||||
_main = new MainThreadState(fileName);
|
||||
_bg = new BackgroundThread(_main, templateEngine, fileName);
|
||||
|
||||
_main.ResultsReady += (sender, args) => OnResultsReady(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired on the main thread.
|
||||
/// </summary>
|
||||
public event EventHandler<DocumentParseCompleteEventArgs> ResultsReady;
|
||||
|
||||
public bool IsIdle
|
||||
{
|
||||
get { return _main.IsIdle; }
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_bg.Start();
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
_main.Cancel();
|
||||
}
|
||||
|
||||
public void QueueChange(TextChange change)
|
||||
{
|
||||
_main.QueueChange(change);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TreesAreDifferent(RazorSyntaxTree leftTree, RazorSyntaxTree rightTree, IEnumerable<TextChange> changes)
|
||||
{
|
||||
return TreesAreDifferent(leftTree, rightTree, changes, CancellationToken.None);
|
||||
}
|
||||
|
||||
internal static bool TreesAreDifferent(RazorSyntaxTree leftTree, RazorSyntaxTree rightTree, IEnumerable<TextChange> changes, CancellationToken cancelToken)
|
||||
{
|
||||
return TreesAreDifferent(leftTree.Root, rightTree.Root, changes, cancelToken);
|
||||
}
|
||||
|
||||
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable<TextChange> changes)
|
||||
{
|
||||
return TreesAreDifferent(leftTree, rightTree, changes, CancellationToken.None);
|
||||
}
|
||||
|
||||
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable<TextChange> 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 (TextChange 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<TextChange> _changes = new List<TextChange>();
|
||||
|
||||
public MainThreadState(string fileName)
|
||||
{
|
||||
_fileName = fileName;
|
||||
|
||||
SetThreadId(Thread.CurrentThread.ManagedThreadId);
|
||||
}
|
||||
|
||||
public event EventHandler<DocumentParseCompleteEventArgs> 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(TextChange change)
|
||||
{
|
||||
EnsureOnThread();
|
||||
lock (_stateLock)
|
||||
{
|
||||
// CurrentParcel token source is not null ==> There's a parse underway
|
||||
if (_currentParcelCancelSource != null)
|
||||
{
|
||||
_currentParcelCancelSource.Cancel();
|
||||
}
|
||||
|
||||
_changes.Add(change);
|
||||
_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<TextChange>();
|
||||
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 _fileName;
|
||||
private RazorSyntaxTree _currentSyntaxTree;
|
||||
private IList<TextChange> _previouslyDiscarded = new List<TextChange>();
|
||||
|
||||
public BackgroundThread(MainThreadState main, RazorTemplateEngine templateEngine, string fileName)
|
||||
{
|
||||
// Run on MAIN thread!
|
||||
_main = main;
|
||||
_shutdownToken = _main.CancelToken;
|
||||
_templateEngine = templateEngine;
|
||||
_fileName = fileName;
|
||||
|
||||
_backgroundThread = new Thread(WorkerLoop);
|
||||
SetThreadId(_backgroundThread.ManagedThreadId);
|
||||
}
|
||||
|
||||
// **** ANY THREAD ****
|
||||
public void Start()
|
||||
{
|
||||
_backgroundThread.Start();
|
||||
}
|
||||
|
||||
// **** BACKGROUND THREAD ****
|
||||
private void WorkerLoop()
|
||||
{
|
||||
var fileNameOnly = Path.GetFileName(_fileName);
|
||||
|
||||
try
|
||||
{
|
||||
EnsureOnThread();
|
||||
|
||||
#if NETSTANDARD1_3
|
||||
var spinWait = new SpinWait();
|
||||
#endif
|
||||
|
||||
while (!_shutdownToken.IsCancellationRequested)
|
||||
{
|
||||
// Grab the parcel of work to do
|
||||
var parcel = _main.GetParcel();
|
||||
if (parcel.Changes.Any())
|
||||
{
|
||||
try
|
||||
{
|
||||
DocumentParseCompleteEventArgs args = null;
|
||||
using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken))
|
||||
{
|
||||
if (!linkedCancel.IsCancellationRequested)
|
||||
{
|
||||
// Collect ALL changes
|
||||
List<TextChange> allChanges;
|
||||
|
||||
if (_previouslyDiscarded != null)
|
||||
{
|
||||
allChanges = Enumerable.Concat(_previouslyDiscarded, parcel.Changes).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
allChanges = parcel.Changes.ToList();
|
||||
}
|
||||
|
||||
var finalChange = allChanges.Last();
|
||||
|
||||
var results = ParseChange(finalChange.NewBuffer, linkedCancel.Token);
|
||||
|
||||
if (results != null && !linkedCancel.IsCancellationRequested)
|
||||
{
|
||||
// Clear discarded changes list
|
||||
_previouslyDiscarded = null;
|
||||
|
||||
var treeStructureChanged = _currentSyntaxTree == null || TreesAreDifferent(_currentSyntaxTree, results.GetSyntaxTree(), allChanges, parcel.CancelToken);
|
||||
_currentSyntaxTree = results.GetSyntaxTree();
|
||||
|
||||
// Build Arguments
|
||||
args = new DocumentParseCompleteEventArgs()
|
||||
{
|
||||
GeneratorResults = results,
|
||||
SourceChange = finalChange,
|
||||
TreeStructureChanged = treeStructureChanged
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse completed but we were cancelled in the mean time. Add these to the discarded changes set
|
||||
_previouslyDiscarded = allChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (args != null)
|
||||
{
|
||||
_main.ReturnParcel(args);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
#if NETSTANDARD1_3
|
||||
// This does the equivalent of thread.yield under the covers.
|
||||
spinWait.SpinOnce();
|
||||
#else
|
||||
// No Yield in CoreCLR
|
||||
|
||||
Thread.Yield();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Do nothing. Just shut down.
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up main thread resources
|
||||
_main.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private RazorCodeDocument ParseChange(ITextBuffer buffer, CancellationToken token)
|
||||
{
|
||||
EnsureOnThread();
|
||||
|
||||
// Seek the buffer to the beginning
|
||||
buffer.Position = 0;
|
||||
|
||||
var sourceDocument = LegacySourceDocument.Create(buffer, _fileName);
|
||||
var imports = _templateEngine.GetImports(_fileName);
|
||||
|
||||
var codeDocument = RazorCodeDocument.Create(sourceDocument, imports);
|
||||
|
||||
_templateEngine.GenerateCode(codeDocument);
|
||||
return codeDocument;
|
||||
}
|
||||
}
|
||||
|
||||
private class WorkParcel
|
||||
{
|
||||
public WorkParcel(IList<TextChange> changes, CancellationToken cancelToken)
|
||||
{
|
||||
Changes = changes;
|
||||
CancelToken = cancelToken;
|
||||
}
|
||||
|
||||
public CancellationToken CancelToken { get; private set; }
|
||||
public IList<TextChange> Changes { get; private set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -102,6 +102,38 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
|||
return current as Span;
|
||||
}
|
||||
|
||||
public virtual Span LocateOwner(TextChange change) => LocateOwner(change, Children);
|
||||
|
||||
protected static Span LocateOwner(TextChange change, IEnumerable<SyntaxTreeNode> elements)
|
||||
{
|
||||
// Ask each child recursively
|
||||
Span owner = null;
|
||||
foreach (var element in elements)
|
||||
{
|
||||
var span = element as Span;
|
||||
if (span == null)
|
||||
{
|
||||
owner = ((Block)element).LocateOwner(change);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (change.OldPosition < span.Start.AbsoluteIndex)
|
||||
{
|
||||
// Early escape for cases where changes overlap multiple spans
|
||||
// In those cases, the span will return false, and we don't want to search the whole tree
|
||||
// So if the current span starts after the change, we know we've searched as far as we need to
|
||||
break;
|
||||
}
|
||||
owner = span.EditHandler.OwnsChange(span, change) ? span : owner;
|
||||
}
|
||||
|
||||
if (owner != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
// 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.Evolution.Legacy
|
||||
{
|
||||
/// <summary>
|
||||
/// Arguments for the DocumentParseComplete event in RazorEditorParser
|
||||
/// </summary>
|
||||
public class DocumentParseCompleteEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates if the tree structure has actually changed since the previous re-parse.
|
||||
/// </summary>
|
||||
public bool TreeStructureChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The result of the parsing and code generation.
|
||||
/// </summary>
|
||||
public RazorCodeDocument GeneratorResults { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The TextChange which triggered the re-parse
|
||||
/// </summary>
|
||||
public TextChange SourceChange { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||
{
|
||||
internal interface ITextBuffer
|
||||
public interface ITextBuffer
|
||||
{
|
||||
int Length { get; }
|
||||
int Position { get; set; }
|
||||
|
|
|
|||
|
|
@ -80,6 +80,11 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
|||
return HandleDotlessCommitInsertion(target);
|
||||
}
|
||||
|
||||
if (IsAcceptableIdentifierReplacement(target, normalizedChange))
|
||||
{
|
||||
return TryAcceptChange(target, normalizedChange);
|
||||
}
|
||||
|
||||
if (IsAcceptableReplace(target, normalizedChange))
|
||||
{
|
||||
return HandleReplacement(target, normalizedChange);
|
||||
|
|
@ -150,6 +155,59 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
|||
(change.IsReplace && RemainingIsWhitespace(target, change));
|
||||
}
|
||||
|
||||
private bool IsAcceptableIdentifierReplacement(Span target, TextChange change)
|
||||
{
|
||||
if (!change.IsReplace)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < target.Symbols.Count; i++)
|
||||
{
|
||||
var symbol = target.Symbols[i] as CSharpSymbol;
|
||||
|
||||
if (symbol == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var symbolStartIndex = symbol.Start.AbsoluteIndex;
|
||||
var symbolEndIndex = symbolStartIndex + symbol.Content.Length;
|
||||
|
||||
// We're looking for the first symbol that contains the TextChange.
|
||||
if (symbolEndIndex > change.OldPosition)
|
||||
{
|
||||
if (symbolEndIndex >= change.OldPosition + change.OldLength && symbol.Type == CSharpSymbolType.Identifier)
|
||||
{
|
||||
// The symbol we're changing happens to be an identifier. Need to check if its transformed state is also one.
|
||||
// We do this transformation logic to capture the case that the new text change happens to not be an identifier;
|
||||
// i.e. "5". Alone, it's numeric, within an identifier it's classified as identifier.
|
||||
var transformedContent = change.ApplyChange(symbol.Content, symbolStartIndex);
|
||||
var newSymbols = Tokenizer(transformedContent);
|
||||
|
||||
if (newSymbols.Count() != 1)
|
||||
{
|
||||
// The transformed content resulted in more than one symbol; we can only replace a single identifier with
|
||||
// another single identifier.
|
||||
break;
|
||||
}
|
||||
|
||||
var newSymbol = (CSharpSymbol)newSymbols.First();
|
||||
if (newSymbol.Type == CSharpSymbolType.Identifier)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Change is touching a non-identifier symbol or spans multiple symbols.
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsAcceptableDeletion(Span target, TextChange change)
|
||||
{
|
||||
return IsEndDeletion(target, change) ||
|
||||
|
|
@ -300,7 +358,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
|||
|
||||
private PartialParseResult TryAcceptChange(Span target, TextChange change, PartialParseResult acceptResult = PartialParseResult.Accepted)
|
||||
{
|
||||
var content = change.ApplyChange(target);
|
||||
var content = change.ApplyChange(target.Content, target.Start.AbsoluteIndex);
|
||||
if (StartsWithKeyword(content))
|
||||
{
|
||||
return PartialParseResult.Rejected | PartialParseResult.SpanContextChanged;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Razor.Evolution;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||
{
|
||||
internal class LegacySourceDocument : RazorSourceDocument
|
||||
{
|
||||
private readonly ITextBuffer _buffer;
|
||||
private readonly string _filename;
|
||||
private readonly RazorSourceLineCollection _lines;
|
||||
|
||||
public static RazorSourceDocument Create(ITextBuffer buffer, string filename)
|
||||
{
|
||||
return new LegacySourceDocument(buffer, filename);
|
||||
}
|
||||
|
||||
private LegacySourceDocument(ITextBuffer buffer, string filename)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_filename = filename;
|
||||
|
||||
_lines = new DefaultRazorSourceLineCollection(this);
|
||||
}
|
||||
|
||||
public override char this[int position]
|
||||
{
|
||||
get
|
||||
{
|
||||
_buffer.Position = position;
|
||||
return (char)_buffer.Read();
|
||||
}
|
||||
}
|
||||
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
|
||||
public override string FileName => _filename;
|
||||
|
||||
public override int Length => _buffer.Length;
|
||||
|
||||
public override RazorSourceLineCollection Lines => _lines;
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
|||
/// Provisional may NOT be set with Rejected and SpanContextChanged may NOT be set with Accepted.
|
||||
/// </remarks>
|
||||
[Flags]
|
||||
internal enum PartialParseResult
|
||||
public enum PartialParseResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the edit could not be accepted and that a reparse is underway.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,260 @@
|
|||
// 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 System.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||
{
|
||||
/// <summary>
|
||||
/// Parser used by editors to avoid reparsing the entire document on each text change.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This parser is designed to allow editors to avoid having to worry about incremental parsing.
|
||||
/// The <see cref="CheckForStructureChanges"/> method can be called with every change made by a user in an editor
|
||||
/// and the parser will provide a result indicating if it was able to incrementally apply the change.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The general workflow for editors with this parser is:
|
||||
/// <list type="number">
|
||||
/// <item><description>User edits document.</description></item>
|
||||
/// <item><description>Editor builds a <see cref="TextChange"/> structure describing the edit and providing a
|
||||
/// reference to the <em>updated</em> text buffer.</description></item>
|
||||
/// <item><description>Editor calls <see cref="CheckForStructureChanges"/> passing in that change.
|
||||
/// </description></item>
|
||||
/// <item><description>Parser determines if the change can be simply applied to an existing parse tree node.
|
||||
/// </description></item>
|
||||
/// <list type="number">
|
||||
/// <item><description>If it can, the Parser updates its parse tree and returns
|
||||
/// <see cref="PartialParseResult.Accepted"/>.</description></item>
|
||||
/// <item><description>If it cannot, the Parser starts a background parse task and returns
|
||||
/// <see cref="PartialParseResult.Rejected"/>.</description></item>
|
||||
/// </list>
|
||||
/// </list>
|
||||
/// NOTE: Additional flags can be applied to the <see cref="PartialParseResult"/>, see that <c>enum</c> for more
|
||||
/// details. However, the <see cref="PartialParseResult.Accepted"/> or <see cref="PartialParseResult.Rejected"/>
|
||||
/// flags will ALWAYS be present.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A change can only be incrementally parsed if a single, unique, span (seein the syntax tree can be identified
|
||||
/// as owning the entire change. For example, if a change overlaps with multiple <see cref="Span"/>s, the change
|
||||
/// cannot be parsed incrementally and a full reparse is necessary. A <see cref="Span"/> "owns" a change if the
|
||||
/// change occurs either a) entirely within it's boundaries or b) it is a pure insertion
|
||||
/// (see <see cref="TextChange"/>) at the end of a <see cref="Span"/> whose <see cref="Span.EditHandler"/> can
|
||||
/// accept the change (see <see cref="SpanEditHandler.CanAcceptChange"/>).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When the <see cref="RazorEditorParser"/> returns <see cref="PartialParseResult.Accepted"/>, it updates
|
||||
/// <see cref="CurrentSyntaxTree"/> immediately. However, the editor is expected to update it's own data structures
|
||||
/// independently. It can use <see cref="CurrentSyntaxTree"/> to do this, as soon as the editor returns from
|
||||
/// <see cref="CheckForStructureChanges"/>, but it should (ideally) have logic for doing so without needing the new
|
||||
/// tree.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When <see cref="PartialParseResult.Rejected"/> is returned by <see cref="CheckForStructureChanges"/>, a
|
||||
/// background parse task has <em>already</em> been started. When that task finishes, the
|
||||
/// <see cref="DocumentParseComplete"/> event will be fired containing the new generated code, parse tree and a
|
||||
/// reference to the original <see cref="TextChange"/> that caused the reparse, to allow the editor to resolve the
|
||||
/// new tree against any changes made since calling <see cref="CheckForStructureChanges"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If a call to <see cref="CheckForStructureChanges"/> occurs while a reparse is already in-progress, the reparse
|
||||
/// is canceled IMMEDIATELY and <see cref="PartialParseResult.Rejected"/> is returned without attempting to
|
||||
/// reparse. This means that if a consumer calls <see cref="CheckForStructureChanges"/>, which returns
|
||||
/// <see cref="PartialParseResult.Rejected"/>, then calls it again before <see cref="DocumentParseComplete"/> is
|
||||
/// fired, it will only receive one <see cref="DocumentParseComplete"/> event, for the second change.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class RazorEditorParser : IDisposable
|
||||
{
|
||||
// Lock for this document
|
||||
private Span _lastChangeOwner;
|
||||
private Span _lastAutoCompleteSpan;
|
||||
private BackgroundParser _parser;
|
||||
private RazorSyntaxTree _currentSyntaxTree;
|
||||
|
||||
public RazorEditorParser(RazorTemplateEngine templateEngine, string sourceFileName)
|
||||
{
|
||||
if (templateEngine == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(templateEngine));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(sourceFileName))
|
||||
{
|
||||
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(sourceFileName));
|
||||
}
|
||||
|
||||
TemplateEngine = templateEngine;
|
||||
FileName = sourceFileName;
|
||||
_parser = new BackgroundParser(templateEngine, sourceFileName);
|
||||
_parser.ResultsReady += (sender, args) => OnDocumentParseComplete(args);
|
||||
_parser.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when a full reparse of the document completes.
|
||||
/// </summary>
|
||||
public event EventHandler<DocumentParseCompleteEventArgs> DocumentParseComplete;
|
||||
|
||||
public RazorTemplateEngine TemplateEngine { get; private set; }
|
||||
public string FileName { get; private set; }
|
||||
public bool LastResultProvisional { get; private set; }
|
||||
|
||||
public RazorSyntaxTree CurrentSyntaxTree => _currentSyntaxTree;
|
||||
|
||||
public virtual string GetAutoCompleteString()
|
||||
{
|
||||
if (_lastAutoCompleteSpan != null)
|
||||
{
|
||||
var editHandler = _lastAutoCompleteSpan.EditHandler as AutoCompleteEditHandler;
|
||||
if (editHandler != null)
|
||||
{
|
||||
return editHandler.AutoCompleteString;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a change will cause a structural change to the document and if not, applies it to the
|
||||
/// existing tree. If a structural change would occur, automatically starts a reparse.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// NOTE: The initial incremental parsing check and actual incremental parsing (if possible) occurs
|
||||
/// on the caller's thread. However, if a full reparse is needed, this occurs on a background thread.
|
||||
/// </remarks>
|
||||
/// <param name="change">The change to apply to the parse tree.</param>
|
||||
/// <returns>A <see cref="PartialParseResult"/> value indicating the result of the incremental parse.</returns>
|
||||
public virtual PartialParseResult CheckForStructureChanges(TextChange change)
|
||||
{
|
||||
var result = PartialParseResult.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 & PartialParseResult.Rejected) == PartialParseResult.Rejected)
|
||||
{
|
||||
_parser.QueueChange(change);
|
||||
}
|
||||
|
||||
// Otherwise, remember if this was provisionally accepted for next partial parse
|
||||
LastResultProvisional = (result & PartialParseResult.Provisional) == PartialParseResult.Provisional;
|
||||
VerifyFlagsAreValid(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of this parser. Should be called when the editor window is closed and the document is unloaded.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_parser.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private PartialParseResult TryPartialParse(TextChange change)
|
||||
{
|
||||
var result = PartialParseResult.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 & PartialParseResult.Rejected) != PartialParseResult.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 = PartialParseResult.Rejected;
|
||||
}
|
||||
else if (_lastChangeOwner != null)
|
||||
{
|
||||
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
|
||||
result = editResult.Result;
|
||||
if ((editResult.Result & PartialParseResult.Rejected) != PartialParseResult.Rejected)
|
||||
{
|
||||
_lastChangeOwner.ReplaceWith(editResult.EditedSpan);
|
||||
}
|
||||
if ((result & PartialParseResult.AutoCompleteBlock) == PartialParseResult.AutoCompleteBlock)
|
||||
{
|
||||
_lastAutoCompleteSpan = _lastChangeOwner;
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastAutoCompleteSpan = null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnDocumentParseComplete(DocumentParseCompleteEventArgs args)
|
||||
{
|
||||
using (_parser.SynchronizeMainThreadState())
|
||||
{
|
||||
_currentSyntaxTree = args.GeneratorResults.GetSyntaxTree();
|
||||
_lastChangeOwner = null;
|
||||
}
|
||||
|
||||
Debug.Assert(args != null, "Event arguments cannot be null");
|
||||
EventHandler<DocumentParseCompleteEventArgs> 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(PartialParseResult result)
|
||||
{
|
||||
Debug.Assert(((result & PartialParseResult.Accepted) == PartialParseResult.Accepted) ||
|
||||
((result & PartialParseResult.Rejected) == PartialParseResult.Rejected),
|
||||
"Partial Parse result does not have either of Accepted or Rejected flags set");
|
||||
Debug.Assert(((result & PartialParseResult.Rejected) == PartialParseResult.Rejected) ||
|
||||
((result & PartialParseResult.SpanContextChanged) != PartialParseResult.SpanContextChanged),
|
||||
"Partial Parse result was Accepted AND had SpanContextChanged flag set");
|
||||
Debug.Assert(((result & PartialParseResult.Rejected) == PartialParseResult.Rejected) ||
|
||||
((result & PartialParseResult.AutoCompleteBlock) != PartialParseResult.AutoCompleteBlock),
|
||||
"Partial Parse result was Accepted AND had AutoCompleteBlock flag set");
|
||||
Debug.Assert(((result & PartialParseResult.Accepted) == PartialParseResult.Accepted) ||
|
||||
((result & PartialParseResult.Provisional) != PartialParseResult.Provisional),
|
||||
"Partial Parse result was Rejected AND had Provisional flag set");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,12 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
|||
{
|
||||
Kind = builder.Kind;
|
||||
Symbols = builder.Symbols;
|
||||
|
||||
for (var i = 0; i <Symbols.Count; i++)
|
||||
{
|
||||
Symbols[i].Parent = this;
|
||||
}
|
||||
|
||||
EditHandler = builder.EditHandler;
|
||||
ChunkGenerator = builder.ChunkGenerator ?? SpanChunkGenerator.Null;
|
||||
_start = builder.Start;
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
|||
|
||||
protected virtual SpanBuilder UpdateSpan(Span target, TextChange normalizedChange)
|
||||
{
|
||||
var newContent = normalizedChange.ApplyChange(target);
|
||||
var newContent = normalizedChange.ApplyChange(target.Content, target.Start.AbsoluteIndex);
|
||||
var newSpan = new SpanBuilder(target);
|
||||
newSpan.ClearSymbols();
|
||||
foreach (ISymbol sym in Tokenizer(newContent))
|
||||
|
|
|
|||
|
|
@ -120,6 +120,45 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
|||
}
|
||||
}
|
||||
|
||||
public override Span LocateOwner(TextChange change)
|
||||
{
|
||||
var oldPosition = change.OldPosition;
|
||||
if (oldPosition < Start.AbsoluteIndex)
|
||||
{
|
||||
// Change occurs prior to the TagHelper.
|
||||
return null;
|
||||
}
|
||||
|
||||
var bodyEndLocation = SourceStartTag?.Start.AbsoluteIndex + SourceStartTag?.Length + base.Length;
|
||||
if (oldPosition > bodyEndLocation)
|
||||
{
|
||||
// Change occurs after the TagHelpers body. End tags for TagHelpers cannot claim ownership of changes
|
||||
// because any change to them impacts whether or not a tag is a TagHelper.
|
||||
return null;
|
||||
}
|
||||
|
||||
var startTagEndLocation = Start.AbsoluteIndex + SourceStartTag?.Length;
|
||||
if (oldPosition < startTagEndLocation)
|
||||
{
|
||||
// Change occurs in the start tag.
|
||||
|
||||
var attributeElements = Attributes
|
||||
.Select(attribute => attribute.Value)
|
||||
.Where(value => value != null);
|
||||
|
||||
return LocateOwner(change, attributeElements);
|
||||
}
|
||||
|
||||
if (oldPosition < bodyEndLocation)
|
||||
{
|
||||
// Change occurs in the body
|
||||
return base.LocateOwner(change);
|
||||
}
|
||||
|
||||
// TagHelper does not contain a Span that can claim ownership.
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ using Microsoft.Extensions.Internal;
|
|||
|
||||
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||
{
|
||||
internal struct TextChange
|
||||
public struct TextChange
|
||||
{
|
||||
private string _newText;
|
||||
private string _oldText;
|
||||
|
|
@ -165,15 +165,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
|||
.Insert(changeRelativePosition, NewText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the text change to the content of the span and returns the new content.
|
||||
/// This method doesn't update the span content.
|
||||
/// </summary>
|
||||
public string ApplyChange(Span span)
|
||||
{
|
||||
return ApplyChange(span.Content, span.Start.AbsoluteIndex);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "({0}:{1}) \"{3}\" -> ({0}:{2}) \"{4}\"", OldPosition, OldLength, NewLength, OldText, NewText);
|
||||
|
|
|
|||
|
|
@ -14,4 +14,8 @@
|
|||
<PackageReference Include="Microsoft.Extensions.HashCodeCombiner.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
|
||||
<PackageReference Include="System.Threading.Thread" Version="$(CoreFxVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,16 @@
|
|||
@{
|
||||
string hello = "Hello, World";
|
||||
}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Simple Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Simple Page</h1>
|
||||
<p>@hello</p>
|
||||
<p>
|
||||
@foreach(char c in hello) {@c}
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
namespace Razor
|
||||
{
|
||||
#line hidden
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
public class Template
|
||||
{
|
||||
#pragma warning disable 219
|
||||
private void __RazorDirectiveTokenHelpers__() {
|
||||
((System.Action)(() => {
|
||||
System.Object __typeHelper = "*, Test";
|
||||
}
|
||||
))();
|
||||
}
|
||||
#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
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,11 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
|||
{
|
||||
private readonly Dictionary<string, RazorProjectItem> _lookup;
|
||||
|
||||
public TestRazorProject()
|
||||
: this(new RazorProjectItem[0])
|
||||
{
|
||||
}
|
||||
|
||||
public TestRazorProject(IList<RazorProjectItem> items)
|
||||
{
|
||||
_lookup = items.ToDictionary(item => item.Path);
|
||||
|
|
|
|||
Loading…
Reference in New Issue