Port the legacy RazorEditorParser

This commit is contained in:
Ryan Nowak 2017-03-15 15:05:41 -07:00
parent 9b3e8d0cda
commit 90b48347a5
17 changed files with 2467 additions and 14 deletions

View File

@ -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; }
}
}
}

View File

@ -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(

View File

@ -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; }
}
}

View File

@ -3,7 +3,7 @@
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal interface ITextBuffer
public interface ITextBuffer
{
int Length { get; }
int Position { get; set; }

View File

@ -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;

View File

@ -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];
}
}
}
}

View File

@ -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.

View File

@ -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");
}
}
}

View File

@ -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;

View File

@ -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))

View File

@ -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()
{

View File

@ -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);

View File

@ -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

View File

@ -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>

View File

@ -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
}
}

View File

@ -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);