forked from aspnet/AspNetWebStack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRazorEditorParser.cs
More file actions
287 lines (263 loc) · 13.9 KB
/
RazorEditorParser.cs
File metadata and controls
287 lines (263 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
// 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.CodeDom;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
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 Span _lastChangeOwner;
private Span _lastAutoCompleteSpan;
private BackgroundParser _parser;
private Block _currentParseTree;
/// <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;
_parser = new BackgroundParser(host, 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 RazorEngineHost Host { get; private set; }
public string FileName { get; private set; }
public bool LastResultProvisional { get; private set; }
public Block CurrentParseTree
{
get { return _currentParseTree; }
}
[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
long? elapsedMs = null;
#if EDITOR_TRACING
Stopwatch sw = new Stopwatch();
sw.Start();
#endif
RazorEditorTrace.TraceLine(RazorResources.Trace_EditorReceivedChange, Path.GetFileName(FileName), change);
if (change.NewBuffer == null)
{
throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture,
RazorResources.Structure_Member_CannotBeNull,
"Buffer",
"TextChange"), "change");
}
PartialParseResult result = PartialParseResult.Rejected;
// If there isn't already a parse underway, try partial-parsing
string changeString = String.Empty;
using (_parser.SynchronizeMainThreadState())
{
// Capture the string value of the change while we're synchronized
changeString = change.ToString();
// Check if we can partial-parse
if (CurrentParseTree != null && _parser.IsIdle)
{
result = TryPartialParse(change);
}
}
// If partial parsing failed or there were outstanding parser tasks, start a full reparse
if (result.HasFlag(PartialParseResult.Rejected))
{
_parser.QueueChange(change);
}
// Otherwise, remember if this was provisionally accepted for next partial parse
LastResultProvisional = result.HasFlag(PartialParseResult.Provisional);
VerifyFlagsAreValid(result);
#if EDITOR_TRACING
sw.Stop();
elapsedMs = sw.ElapsedMilliseconds;
sw.Reset();
#endif
RazorEditorTrace.TraceLine(RazorResources.Trace_EditorProcessedChange, Path.GetFileName(FileName), changeString, elapsedMs.HasValue ? elapsedMs.Value.ToString(CultureInfo.InvariantCulture) : "?", result.ToString());
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)
{
_parser.Dispose();
}
}
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);
}
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;
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are being caught here intentionally")]
private void OnDocumentParseComplete(DocumentParseCompleteEventArgs args)
{
using (_parser.SynchronizeMainThreadState())
{
_currentParseTree = args.GeneratorResults.Document;
_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.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");
}
}
}