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;
|
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()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format(
|
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
|
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
{
|
{
|
||||||
internal interface ITextBuffer
|
public interface ITextBuffer
|
||||||
{
|
{
|
||||||
int Length { get; }
|
int Length { get; }
|
||||||
int Position { get; set; }
|
int Position { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,11 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
return HandleDotlessCommitInsertion(target);
|
return HandleDotlessCommitInsertion(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsAcceptableIdentifierReplacement(target, normalizedChange))
|
||||||
|
{
|
||||||
|
return TryAcceptChange(target, normalizedChange);
|
||||||
|
}
|
||||||
|
|
||||||
if (IsAcceptableReplace(target, normalizedChange))
|
if (IsAcceptableReplace(target, normalizedChange))
|
||||||
{
|
{
|
||||||
return HandleReplacement(target, normalizedChange);
|
return HandleReplacement(target, normalizedChange);
|
||||||
|
|
@ -150,6 +155,59 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
(change.IsReplace && RemainingIsWhitespace(target, change));
|
(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)
|
private static bool IsAcceptableDeletion(Span target, TextChange change)
|
||||||
{
|
{
|
||||||
return IsEndDeletion(target, 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)
|
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))
|
if (StartsWithKeyword(content))
|
||||||
{
|
{
|
||||||
return PartialParseResult.Rejected | PartialParseResult.SpanContextChanged;
|
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.
|
/// Provisional may NOT be set with Rejected and SpanContextChanged may NOT be set with Accepted.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[Flags]
|
[Flags]
|
||||||
internal enum PartialParseResult
|
public enum PartialParseResult
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates that the edit could not be accepted and that a reparse is underway.
|
/// 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;
|
Kind = builder.Kind;
|
||||||
Symbols = builder.Symbols;
|
Symbols = builder.Symbols;
|
||||||
|
|
||||||
|
for (var i = 0; i <Symbols.Count; i++)
|
||||||
|
{
|
||||||
|
Symbols[i].Parent = this;
|
||||||
|
}
|
||||||
|
|
||||||
EditHandler = builder.EditHandler;
|
EditHandler = builder.EditHandler;
|
||||||
ChunkGenerator = builder.ChunkGenerator ?? SpanChunkGenerator.Null;
|
ChunkGenerator = builder.ChunkGenerator ?? SpanChunkGenerator.Null;
|
||||||
_start = builder.Start;
|
_start = builder.Start;
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
|
|
||||||
protected virtual SpanBuilder UpdateSpan(Span target, TextChange normalizedChange)
|
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);
|
var newSpan = new SpanBuilder(target);
|
||||||
newSpan.ClearSymbols();
|
newSpan.ClearSymbols();
|
||||||
foreach (ISymbol sym in Tokenizer(newContent))
|
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 />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ using Microsoft.Extensions.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
{
|
{
|
||||||
internal struct TextChange
|
public struct TextChange
|
||||||
{
|
{
|
||||||
private string _newText;
|
private string _newText;
|
||||||
private string _oldText;
|
private string _oldText;
|
||||||
|
|
@ -165,15 +165,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
.Insert(changeRelativePosition, NewText);
|
.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()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format(CultureInfo.CurrentCulture, "({0}:{1}) \"{3}\" -> ({0}:{2}) \"{4}\"", OldPosition, OldLength, NewLength, OldText, NewText);
|
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" />
|
<PackageReference Include="Microsoft.Extensions.HashCodeCombiner.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
|
||||||
|
<PackageReference Include="System.Threading.Thread" Version="$(CoreFxVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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;
|
private readonly Dictionary<string, RazorProjectItem> _lookup;
|
||||||
|
|
||||||
|
public TestRazorProject()
|
||||||
|
: this(new RazorProjectItem[0])
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public TestRazorProject(IList<RazorProjectItem> items)
|
public TestRazorProject(IList<RazorProjectItem> items)
|
||||||
{
|
{
|
||||||
_lookup = items.ToDictionary(item => item.Path);
|
_lookup = items.ToDictionary(item => item.Path);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue