// 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
{
///
/// Parser used by editors to avoid reparsing the entire document on each text change
///
///
/// 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.
///
public class RazorEditorParser : IDisposable
{
// Lock for this document
private object _lock = new object();
private Stack _outstandingParserTasks = new Stack();
private Span _lastChangeOwner;
private Span _lastAutoCompleteSpan;
#if DEBUG
private CodeCompileUnit _currentCompileUnit;
#endif
///
/// 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.
///
/// The which defines the environment in which the generated code will live. should be set if design-time code mappings are desired
/// The physical path to use in line pragmas
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;
}
///
/// Event fired when a full reparse of the document completes
///
public event EventHandler 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;
}
///
/// 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
///
///
/// 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.
///
/// The change to apply to the parse tree
/// A PartialParseResult value indicating the result of the incremental parse
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;
}
///
/// Disposes of this parser. Should be called when the editor window is closed and the document is unloaded.
///
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 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);
}
}
}