2
// RazorCSharpParser.cs
5
// Piotr Dowgiallo <sparekd@gmail.com>
7
// Copyright (c) 2012 Piotr Dowgiallo
9
// Permission is hereby granted, free of charge, to any person obtaining a copy
10
// of this software and associated documentation files (the "Software"), to deal
11
// in the Software without restriction, including without limitation the rights
12
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
// copies of the Software, and to permit persons to whom the Software is
14
// furnished to do so, subject to the following conditions:
16
// The above copyright notice and this permission notice shall be included in
17
// all copies or substantial portions of the Software.
19
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
using System.Collections.Generic;
31
using MonoDevelop.Ide.TypeSystem;
32
using System.Web.Razor;
33
using System.Web.Mvc.Razor;
34
using System.Threading;
35
using System.Web.Razor.Text;
36
using MonoDevelop.Ide;
37
using Mono.TextEditor;
38
using MonoDevelop.Ide.Gui;
39
using ICSharpCode.NRefactory.TypeSystem;
40
using System.Web.Razor.Parser;
41
using System.Web.Razor.Parser.SyntaxTree;
43
using MonoDevelop.Core;
44
using MonoDevelop.AspNet.Parser;
45
using System.Web.Configuration;
46
using System.Web.WebPages.Razor.Configuration;
47
using System.Web.WebPages.Razor;
48
using System.Configuration;
49
using MonoDevelop.Projects;
50
using MonoDevelop.AspNet.StateEngine;
52
namespace MonoDevelop.AspNet.Mvc.Parser
54
public class RazorCSharpParser : TypeSystemParser
56
RazorEditorParserFixed.RazorEditorParser editorParser;
57
DocumentParseCompleteEventArgs capturedArgs;
58
AutoResetEvent parseComplete;
59
ChangeInfo lastChange;
60
string lastParsedFile;
61
TextDocument currentDocument;
62
AspMvcProject aspProject;
63
DotNetProject project;
64
IList<TextDocument> openDocuments;
66
public IList<TextDocument> OpenDocuments { get { return openDocuments; } }
68
public RazorCSharpParser ()
70
openDocuments = new List<TextDocument> ();
72
IdeApp.Exited += delegate {
73
//HACK: workaround for Mono's not shutting downs IsBackground threads in WaitAny calls
74
if (editorParser != null) {
75
DisposeCurrentParser ();
80
public override ParsedDocument Parse (bool storeAst, string fileName, System.IO.TextReader content, Projects.Project project = null)
82
currentDocument = openDocuments.FirstOrDefault (d => d != null && d.FileName == fileName);
83
// We need document and project to be loaded to correctly initialize Razor Host.
84
this.project = project as DotNetProject;
85
if (this.project == null || (currentDocument == null && !TryAddDocument (fileName)))
86
return new RazorCSharpParsedDocument (fileName, new RazorCSharpPageInfo ());
88
this.aspProject = project as AspMvcProject;
90
EnsureParserInitializedFor (fileName);
92
var errors = new List<Error> ();
94
using (var source = new SeekableTextReader (content)) {
95
var textChange = CreateTextChange (source);
96
var parseResult = editorParser.CheckForStructureChanges (textChange);
97
if (parseResult == PartialParseResult.Rejected) {
98
parseComplete.WaitOne ();
99
if (!capturedArgs.GeneratorResults.Success)
100
GetRazorErrors (errors);
104
ParseHtmlDocument (errors);
105
CreateCSharpParsedDocument ();
108
RazorHostKind kind = RazorHostKind.WebPage;
109
if (editorParser.Host is WebCodeRazorHost) {
110
kind = RazorHostKind.WebCode;
111
} else if (editorParser.Host is MonoDevelop.RazorGenerator.RazorHost) {
112
kind = RazorHostKind.Template;
115
var pageInfo = new RazorCSharpPageInfo () {
116
HtmlRoot = htmlParsedDocument,
117
GeneratorResults = capturedArgs.GeneratorResults,
118
Spans = editorParser.CurrentParseTree.Flatten (),
119
CSharpParsedFile = parsedCodeFile,
120
CSharpCode = csharpCode,
122
FoldingRegions = GetFoldingRegions (),
124
Compilation = CreateCompilation (),
128
return new RazorCSharpParsedDocument (fileName, pageInfo);
131
bool TryAddDocument (string fileName)
133
var guiDoc = IdeApp.Workbench.GetDocument (fileName);
134
if (guiDoc != null && guiDoc.Editor != null) {
135
currentDocument = guiDoc.Editor.Document;
136
currentDocument.TextReplacing += OnTextReplacing;
138
var newDocs = new List<TextDocument> (openDocuments);
139
newDocs.Add (currentDocument);
140
openDocuments = newDocs;
142
guiDoc.Closed += (sender, args) =>
144
var doc = sender as Document;
145
if (doc.Editor != null && doc.Editor.Document != null) {
147
openDocuments = new List<TextDocument> (openDocuments.Where (d => d != doc.Editor.Document));
151
if (lastParsedFile == doc.FileName && editorParser != null) {
152
DisposeCurrentParser ();
160
void EnsureParserInitializedFor (string fileName)
162
if (lastParsedFile == fileName && editorParser != null)
165
if (editorParser != null)
166
DisposeCurrentParser ();
168
CreateParserFor (fileName);
171
void CreateParserFor (string fileName)
173
editorParser = new RazorEditorParserFixed.RazorEditorParser (CreateRazorHost (fileName), fileName);
175
parseComplete = new AutoResetEvent (false);
176
editorParser.DocumentParseComplete += (sender, args) =>
179
parseComplete.Set ();
182
lastParsedFile = fileName;
185
RazorEngineHost CreateRazorHost (string fileName)
187
var projectFile = project.GetProjectFile (fileName);
188
if (projectFile != null && projectFile.Generator == "RazorTemplatePreprocessor") {
189
var h = MonoDevelop.RazorGenerator.PreprocessedRazorHost.Create (fileName);
190
h.DesignTimeMode = true;
191
h.EnableLinePragmas = false;
195
string virtualPath = "~/Views/Default.cshtml";
196
if (aspProject != null)
197
virtualPath = aspProject.LocalToVirtualPath (fileName);
199
WebPageRazorHost host = null;
201
// Try to create host using web.config file
202
var webConfigMap = new WebConfigurationFileMap ();
203
if (aspProject != null) {
204
var vdm = new VirtualDirectoryMapping (aspProject.BaseDirectory.Combine ("Views"), true, "web.config");
205
webConfigMap.VirtualDirectories.Add ("/", vdm);
207
Configuration configuration;
209
configuration = WebConfigurationManager.OpenMappedWebConfiguration (webConfigMap, "/");
211
configuration = null;
213
if (configuration != null) {
214
var rws = configuration.GetSectionGroup (RazorWebSectionGroup.GroupName) as RazorWebSectionGroup;
216
host = WebRazorHostFactory.CreateHostFromConfig (rws, virtualPath, fileName);
217
host.DesignTimeMode = true;
222
host = new MvcWebPageRazorHost (virtualPath, fileName) { DesignTimeMode = true };
223
// Add default namespaces from Razor section
224
host.NamespaceImports.Add ("System.Web.Mvc");
225
host.NamespaceImports.Add ("System.Web.Mvc.Ajax");
226
host.NamespaceImports.Add ("System.Web.Mvc.Html");
227
host.NamespaceImports.Add ("System.Web.Routing");
233
void DisposeCurrentParser ()
235
editorParser.Dispose ();
237
parseComplete.Dispose ();
238
parseComplete = null;
242
void ClearLastChange ()
247
TextChange CreateTextChange (SeekableTextReader source)
249
if (lastChange == null)
250
return new TextChange (0, 0, new SeekableTextReader (String.Empty), 0, source.Length, source);
251
if (lastChange.DeleteChange)
252
return new TextChange (lastChange.StartOffset, lastChange.AbsoluteLength, lastChange.Buffer,
253
lastChange.StartOffset, 0, source);
254
return new TextChange (lastChange.StartOffset, 0, lastChange.Buffer, lastChange.StartOffset,
255
lastChange.AbsoluteLength, source);
258
void GetRazorErrors (List<Error> errors)
260
foreach (var error in capturedArgs.GeneratorResults.ParserErrors) {
261
int off = error.Location.AbsoluteIndex;
262
if (error.Location.CharacterIndex > 0 && error.Length == 1)
264
errors.Add (new Error (ErrorType.Error, error.Message, currentDocument.OffsetToLocation (off)));
268
Xml.StateEngine.XDocument htmlParsedDocument;
269
IList<Comment> comments;
271
void ParseHtmlDocument (List<Error> errors)
273
var sb = new StringBuilder ();
274
var spanList = new List<Span> ();
275
comments = new List<Comment> ();
277
Action<Span> action = (Span span) =>
279
if (span.Kind == SpanKind.Markup) {
280
sb.Append (span.Content);
283
for (int i = 0; i < span.Content.Length; i++) {
284
char ch = span.Content[i];
285
if (ch != '\r' && ch != '\n')
290
if (span.Kind == SpanKind.Comment) {
291
var comment = new Comment (span.Content)
295
CommentType = CommentType.Block,
297
comment.Region = new DomRegion (
298
currentDocument.OffsetToLocation (span.Start.AbsoluteIndex - comment.OpenTag.Length),
299
currentDocument.OffsetToLocation (span.Start.AbsoluteIndex + span.Length + comment.ClosingTag.Length));
300
comments.Add (comment);
305
editorParser.CurrentParseTree.Accept (new CallbackVisitor (action));
307
var parser = new Xml.StateEngine.Parser (new AspNetFreeState (), true);
310
parser.Parse (new StringReader (sb.ToString ()));
311
} catch (Exception ex) {
312
LoggingService.LogError ("Unhandled error parsing html in Razor document '" + (lastParsedFile ?? "") + "'", ex);
315
htmlParsedDocument = parser.Nodes.GetRoot ();
316
errors.AddRange (parser.Errors);
319
IEnumerable<FoldingRegion> GetFoldingRegions ()
321
var foldingRegions = new List<FoldingRegion> ();
322
GetHtmlFoldingRegions (foldingRegions);
323
GetRazorFoldingRegions (foldingRegions);
324
return foldingRegions;
327
void GetHtmlFoldingRegions (List<FoldingRegion> foldingRegions)
329
if (htmlParsedDocument != null) {
330
var d = new AspNetParsedDocument (null, WebSubtype.Html, null, htmlParsedDocument);
331
foldingRegions.AddRange (d.Foldings);
335
void GetRazorFoldingRegions (List<FoldingRegion> foldingRegions)
337
var blocks = new List<Block> ();
338
GetBlocks (editorParser.CurrentParseTree, blocks);
339
foreach (var block in blocks) {
340
var beginLine = currentDocument.GetLineByOffset (block.Start.AbsoluteIndex);
341
var endLine = currentDocument.GetLineByOffset (block.Start.AbsoluteIndex + block.Length);
342
if (beginLine != endLine)
343
foldingRegions.Add (new FoldingRegion (RazorUtils.GetShortName (block),
344
new DomRegion (currentDocument.OffsetToLocation (block.Start.AbsoluteIndex),
345
currentDocument.OffsetToLocation (block.Start.AbsoluteIndex + block.Length))));
349
void GetBlocks (Block root, IList<Block> blocks)
351
foreach (var block in root.Children.Where (n => n.IsBlock).Select (n => n as Block)) {
352
if (block.Type != BlockType.Comment && block.Type != BlockType.Markup)
354
if (block.Type != BlockType.Helper)
355
GetBlocks (block, blocks);
359
ParsedDocumentDecorator parsedCodeFile;
362
void CreateCSharpParsedDocument ()
364
var parser = new ICSharpCode.NRefactory.CSharp.CSharpParser ();
365
ICSharpCode.NRefactory.CSharp.SyntaxTree unit;
366
csharpCode = CreateCodeFile ();
367
using (var sr = new StringReader (csharpCode)) {
368
unit = parser.Parse (sr, "Generated.cs");
371
var parsedDoc = unit.ToTypeSystem ();
372
parsedCodeFile = new ParsedDocumentDecorator (parsedDoc) { Ast = unit };
375
string CreateCodeFile ()
377
var unit = capturedArgs.GeneratorResults.GeneratedCode;
378
var provider = project.LanguageBinding.GetCodeDomProvider ();
379
using (var sw = new StringWriter ()) {
380
provider.GenerateCodeFromCompileUnit (unit, sw, new System.CodeDom.Compiler.CodeGeneratorOptions () {
381
// HACK: we use true, even though razor uses false, to work around a mono bug where it omits the
382
// line ending after "#line hidden", resulting in the unparseable "#line hiddenpublic"
383
BlankLinesBetweenMembers = true,
384
// matches Razor built-in settings
385
IndentString = String.Empty,
387
return sw.ToString ();
391
// Creates compilation that includes underlying C# file for Razor view
392
ICompilation CreateCompilation ()
394
return TypeSystemService.GetProjectContext (project).AddOrUpdateFiles (parsedCodeFile.ParsedFile).CreateCompilation ();
397
void OnTextReplacing (object sender, DocumentChangeEventArgs e)
399
if (lastChange == null)
400
lastChange = new ChangeInfo (e.Offset, new SeekableTextReader((sender as TextDocument).Text));
401
if (e.ChangeDelta > 0) {
402
lastChange.Length += e.InsertionLength;
404
lastChange.Length -= e.RemovalLength;
413
public ChangeInfo (int off, SeekableTextReader buffer)
420
public int StartOffset {
421
get { return offset; }
425
public int Length { get; set; }
426
public int AbsoluteLength {
427
get { return Math.Abs (Length); }
431
public SeekableTextReader Buffer { get; set; }
432
public bool DeleteChange { get { return Length < 0; } }