// 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); } } }