You've already forked linux-packaging-mono
369 lines
17 KiB
C#
369 lines
17 KiB
C#
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
|
|
|
|
using System.CodeDom;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Web.Razor.Editor;
|
|
using System.Web.Razor.Parser.SyntaxTree;
|
|
using System.Web.Razor.Resources;
|
|
using System.Web.Razor.Text;
|
|
using Microsoft.Internal.Web.Utils;
|
|
|
|
namespace System.Web.Razor
|
|
{
|
|
/// <summary>
|
|
/// Parser used by editors to avoid reparsing the entire document on each text change
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This parser is designed to allow editors to avoid having to worry about incremental parsing.
|
|
/// The 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 reparse the document.
|
|
///
|
|
/// The general workflow for editors with this parser is:
|
|
/// 0. User edits document
|
|
/// 1. Editor builds TextChange structure describing the edit and providing a reference to the _updated_ text buffer
|
|
/// 2. Editor calls CheckForStructureChanges passing in that change.
|
|
/// 3. Parser determines if the change can be simply applied to an existing parse tree node
|
|
/// a. If it can, the Parser updates its parse tree and returns PartialParseResult.Accepted
|
|
/// b. If it can not, the Parser starts a background parse task and return PartialParseResult.Rejected
|
|
/// NOTE: Additional flags can be applied to the PartialParseResult, see that enum for more details. However,
|
|
/// the Accepted or Rejected flags will ALWAYS be present
|
|
///
|
|
/// A change can only be incrementally parsed if a single, unique, Span (see System.Web.Razor.Parser.SyntaxTree) in the syntax tree can
|
|
/// be identified as owning the entire change. For example, if a change overlaps with multiple spans, the change cannot be
|
|
/// parsed incrementally and a full reparse is necessary. A Span "owns" a change if the change occurs either a) entirely
|
|
/// within it's boundaries or b) it is a pure insertion (see TextChange) at the end of a Span whose CanGrow flag (see Span) is
|
|
/// true.
|
|
///
|
|
/// Even if a single unique Span owner can be identified, it's possible the edit will cause the Span to split or merge with other
|
|
/// Spans, in which case, a full reparse is necessary to identify the extent of the changes to the tree.
|
|
///
|
|
/// When the RazorEditorParser returns Accepted, it updates CurrentParseTree immediately. However, the editor is expected to
|
|
/// update it's own data structures independently. It can use CurrentParseTree to do this, as soon as the editor returns from
|
|
/// CheckForStructureChanges, but it should (ideally) have logic for doing so without needing the new tree.
|
|
///
|
|
/// When Rejected is returned by CheckForStructureChanges, a background parse task has _already_ been started. When that task
|
|
/// finishes, the DocumentStructureChanged event will be fired containing the new generated code, parse tree and a reference to
|
|
/// the original TextChange that caused the reparse, to allow the editor to resolve the new tree against any changes made since
|
|
/// calling CheckForStructureChanges.
|
|
///
|
|
/// If a call to CheckForStructureChanges occurs while a reparse is already in-progress, the reparse is cancelled IMMEDIATELY
|
|
/// and Rejected is returned without attempting to reparse. This means that if a conusmer calls CheckForStructureChanges, which
|
|
/// returns Rejected, then calls it again before DocumentParseComplete is fired, it will only recieve one DocumentParseComplete
|
|
/// event, for the second change.
|
|
/// </remarks>
|
|
public class RazorEditorParser : IDisposable
|
|
{
|
|
// Lock for this document
|
|
private object _lock = new object();
|
|
private Stack<BackgroundParseTask> _outstandingParserTasks = new Stack<BackgroundParseTask>();
|
|
private Span _lastChangeOwner;
|
|
private Span _lastAutoCompleteSpan;
|
|
#if DEBUG
|
|
private CodeCompileUnit _currentCompileUnit;
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Constructs the editor parser. One instance should be used per active editor. This
|
|
/// instance _can_ be shared among reparses, but should _never_ be shared between documents.
|
|
/// </summary>
|
|
/// <param name="host">The <see cref="RazorEngineHost"/> which defines the environment in which the generated code will live. <see cref="F:RazorEngineHost.DesignTimeMode"/> should be set if design-time code mappings are desired</param>
|
|
/// <param name="sourceFileName">The physical path to use in line pragmas</param>
|
|
public RazorEditorParser(RazorEngineHost host, string sourceFileName)
|
|
{
|
|
if (host == null)
|
|
{
|
|
throw new ArgumentNullException("host");
|
|
}
|
|
if (String.IsNullOrEmpty(sourceFileName))
|
|
{
|
|
throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "sourceFileName");
|
|
}
|
|
|
|
Host = host;
|
|
FileName = sourceFileName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event fired when a full reparse of the document completes
|
|
/// </summary>
|
|
public event EventHandler<DocumentParseCompleteEventArgs> DocumentParseComplete;
|
|
|
|
public RazorEngineHost Host { get; private set; }
|
|
public string FileName { get; private set; }
|
|
public bool LastResultProvisional { get; private set; }
|
|
public Block CurrentParseTree { get; private set; }
|
|
|
|
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Since this method is heavily affected by side-effects, particularly calls to CheckForStructureChanges, it should not be made into a property")]
|
|
public virtual string GetAutoCompleteString()
|
|
{
|
|
if (_lastAutoCompleteSpan != null)
|
|
{
|
|
AutoCompleteEditHandler 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 callers 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 PartialParseResult value indicating the result of the incremental parse</returns>
|
|
public virtual PartialParseResult CheckForStructureChanges(TextChange change)
|
|
{
|
|
// Validate the change
|
|
if (change.NewBuffer == null)
|
|
{
|
|
throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture,
|
|
RazorResources.Structure_Member_CannotBeNull,
|
|
"Buffer",
|
|
"TextChange"), "change");
|
|
}
|
|
|
|
PartialParseResult result = PartialParseResult.Rejected;
|
|
|
|
// Lock the state objects
|
|
lock (_lock)
|
|
{
|
|
// If there isn't already a parse underway, try partial-parsing
|
|
if (CurrentParseTree != null && _outstandingParserTasks.Count == 0)
|
|
{
|
|
result = TryPartialParse(change);
|
|
}
|
|
|
|
// If partial parsing failed or there were outstanding parser tasks, start a full reparse
|
|
if (result.HasFlag(PartialParseResult.Rejected))
|
|
{
|
|
QueueFullReparse(change);
|
|
}
|
|
#if DEBUG
|
|
else
|
|
{
|
|
if (CurrentParseTree != null)
|
|
{
|
|
RazorDebugHelpers.WriteDebugTree(FileName, CurrentParseTree, result, change, this, false);
|
|
}
|
|
if (_currentCompileUnit != null)
|
|
{
|
|
RazorDebugHelpers.WriteGeneratedCode(FileName, _currentCompileUnit);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Otherwise, remember if this was provisionally accepted for next partial parse
|
|
LastResultProvisional = result.HasFlag(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);
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_cancelTokenSource", Justification = "The cancellation token is owned by the worker thread, so it is disposed there")]
|
|
[SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_changeReceived", Justification = "The change received event is owned by the worker thread, so it is disposed there")]
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (_outstandingParserTasks.Count > 0)
|
|
{
|
|
foreach (var task in _outstandingParserTasks)
|
|
{
|
|
task.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void QueueFullReparse(TextChange change)
|
|
{
|
|
if (_outstandingParserTasks.Count > 0)
|
|
{
|
|
_outstandingParserTasks.Peek().Cancel();
|
|
}
|
|
_outstandingParserTasks.Push(CreateBackgroundTask(Host, FileName, change).ContinueWith(OnParseCompleted));
|
|
}
|
|
|
|
private PartialParseResult TryPartialParse(TextChange change)
|
|
{
|
|
PartialParseResult result = PartialParseResult.Rejected;
|
|
|
|
// Try the last change owner
|
|
if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change))
|
|
{
|
|
EditResult editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
|
|
result = editResult.Result;
|
|
if (!editResult.Result.HasFlag(PartialParseResult.Rejected))
|
|
{
|
|
_lastChangeOwner.ReplaceWith(editResult.EditedSpan);
|
|
}
|
|
|
|
// If the last change was provisional, then the result of this span's attempt to parse partially goes
|
|
// Otherwise, accept the change if this span accepted it, but if it didn't, just do the standard search.
|
|
if (LastResultProvisional || result.HasFlag(PartialParseResult.Accepted))
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Locate the span responsible for this change
|
|
_lastChangeOwner = CurrentParseTree.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)
|
|
{
|
|
EditResult editRes = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
|
|
result = editRes.Result;
|
|
if (!editRes.Result.HasFlag(PartialParseResult.Rejected))
|
|
{
|
|
_lastChangeOwner.ReplaceWith(editRes.EditedSpan);
|
|
}
|
|
if (result.HasFlag(PartialParseResult.AutoCompleteBlock))
|
|
{
|
|
_lastAutoCompleteSpan = _lastChangeOwner;
|
|
}
|
|
else
|
|
{
|
|
_lastAutoCompleteSpan = null;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private void OnParseCompleted(GeneratorResults results, BackgroundParseTask parseTask)
|
|
{
|
|
try
|
|
{
|
|
// Lock the state objects
|
|
bool treeStructureChanged = true;
|
|
TextChange lastChange;
|
|
lock (_lock)
|
|
{
|
|
// Are we still active?
|
|
if (_outstandingParserTasks.Count == 0 || !ReferenceEquals(parseTask, _outstandingParserTasks.Peek()))
|
|
{
|
|
// We aren't, just abort
|
|
return;
|
|
}
|
|
|
|
// Ok, we're active. Flush out the changes from all the parser tasks and clear the stack of outstanding parse tasks
|
|
TextChange[] changes = _outstandingParserTasks.Select(t => t.Change).Reverse().ToArray();
|
|
lastChange = changes.Last();
|
|
_outstandingParserTasks.Clear();
|
|
|
|
// Take the current tree and check for differences
|
|
treeStructureChanged = CurrentParseTree == null || TreesAreDifferent(CurrentParseTree, results.Document, changes);
|
|
CurrentParseTree = results.Document;
|
|
#if DEBUG
|
|
_currentCompileUnit = results.GeneratedCode;
|
|
#endif
|
|
_lastChangeOwner = null;
|
|
}
|
|
|
|
// Done, now exit the lock and fire the event
|
|
OnDocumentParseComplete(new DocumentParseCompleteEventArgs()
|
|
{
|
|
GeneratorResults = results,
|
|
SourceChange = lastChange,
|
|
TreeStructureChanged = treeStructureChanged
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
parseTask.Dispose();
|
|
}
|
|
}
|
|
|
|
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, TextChange[] changes)
|
|
{
|
|
// 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)
|
|
{
|
|
Span changeOwner = leftTree.LocateOwner(change);
|
|
|
|
// Apply the change to the tree
|
|
if (changeOwner == null)
|
|
{
|
|
return true;
|
|
}
|
|
EditResult result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true);
|
|
changeOwner.ReplaceWith(result.EditedSpan);
|
|
}
|
|
|
|
// Now compare the trees
|
|
bool treesDifferent = !leftTree.EquivalentTo(rightTree);
|
|
#if DEBUG
|
|
if (RazorDebugHelpers.OutputDebuggingEnabled)
|
|
{
|
|
Debug.WriteLine(String.Format(CultureInfo.CurrentCulture, "Processed {0} changes, trees were{1} different", changes.Length, treesDifferent ? String.Empty : " not"));
|
|
}
|
|
#endif
|
|
return treesDifferent;
|
|
}
|
|
|
|
private void OnDocumentParseComplete(DocumentParseCompleteEventArgs args)
|
|
{
|
|
Debug.Assert(args != null, "Event arguments cannot be null");
|
|
EventHandler<DocumentParseCompleteEventArgs> handler = DocumentParseComplete;
|
|
if (handler != null)
|
|
{
|
|
handler(this, args);
|
|
}
|
|
#if DEBUG
|
|
RazorDebugHelpers.WriteDebugTree(FileName, args.GeneratorResults.Document, PartialParseResult.Rejected, args.SourceChange, this, args.TreeStructureChanged);
|
|
RazorDebugHelpers.WriteGeneratedCode(FileName, args.GeneratorResults.GeneratedCode);
|
|
#endif
|
|
}
|
|
|
|
[Conditional("DEBUG")]
|
|
private static void VerifyFlagsAreValid(PartialParseResult result)
|
|
{
|
|
Debug.Assert(result.HasFlag(PartialParseResult.Accepted) ||
|
|
result.HasFlag(PartialParseResult.Rejected),
|
|
"Partial Parse result does not have either of Accepted or Rejected flags set");
|
|
Debug.Assert(result.HasFlag(PartialParseResult.Rejected) ||
|
|
!result.HasFlag(PartialParseResult.SpanContextChanged),
|
|
"Partial Parse result was Accepted AND had SpanContextChanged flag set");
|
|
Debug.Assert(result.HasFlag(PartialParseResult.Rejected) ||
|
|
!result.HasFlag(PartialParseResult.AutoCompleteBlock),
|
|
"Partial Parse result was Accepted AND had AutoCompleteBlock flag set");
|
|
Debug.Assert(result.HasFlag(PartialParseResult.Accepted) ||
|
|
!result.HasFlag(PartialParseResult.Provisional),
|
|
"Partial Parse result was Rejected AND had Provisional flag set");
|
|
}
|
|
|
|
internal virtual BackgroundParseTask CreateBackgroundTask(RazorEngineHost host, string fileName, TextChange change)
|
|
{
|
|
return BackgroundParseTask.StartNew(new RazorTemplateEngine(Host), FileName, change);
|
|
}
|
|
}
|
|
}
|