From 7a742d922f7654843d1a02dec49d7106979695ff Mon Sep 17 00:00:00 2001 From: Matt Ward Date: Sun, 16 Sep 2012 18:42:05 +0100 Subject: [PATCH] Add C# Razor completion for model. Use the @model directive to generate a strongly typed WebViewPage class. Support completion on the Model property in a view. --- .../AspNet.Mvc/Project/AspNet.Mvc.csproj | 3 + .../RazorCSharpDotCompletionDataProvider.cs | 34 +++- .../Completion/RazorCSharpModelTypeLocater.cs | 34 ++++ .../Src/Completion/RazorCSharpParser.cs | 5 +- .../RazorCSharpParserModelTypeVisitor.cs | 59 +++++++ .../Src/Completion/RazorCompilationUnit.cs | 34 ++++ .../AspNet.Mvc/Test/AspNet.Mvc.Tests.csproj | 2 + .../Src/Completion/RazorCSharpParserTests.cs | 148 ++++++++++++++++++ 8 files changed, 310 insertions(+), 9 deletions(-) create mode 100644 src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpModelTypeLocater.cs create mode 100644 src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpParserModelTypeVisitor.cs create mode 100644 src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCompilationUnit.cs create mode 100644 src/AddIns/BackendBindings/AspNet.Mvc/Test/Src/Completion/RazorCSharpParserTests.cs diff --git a/src/AddIns/BackendBindings/AspNet.Mvc/Project/AspNet.Mvc.csproj b/src/AddIns/BackendBindings/AspNet.Mvc/Project/AspNet.Mvc.csproj index 1f159d0b72..545ecdcfca 100644 --- a/src/AddIns/BackendBindings/AspNet.Mvc/Project/AspNet.Mvc.csproj +++ b/src/AddIns/BackendBindings/AspNet.Mvc/Project/AspNet.Mvc.csproj @@ -122,10 +122,13 @@ + + + diff --git a/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpDotCompletionDataProvider.cs b/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpDotCompletionDataProvider.cs index 0ed8a6889d..e2aba9cad3 100644 --- a/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpDotCompletionDataProvider.cs +++ b/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpDotCompletionDataProvider.cs @@ -31,7 +31,7 @@ namespace ICSharpCode.AspNet.Mvc.Completion ParseInformation CreateParseInformationWithWebViewPageClass(ParseInformation parseInfo) { - var compilationUnit = new DefaultCompilationUnit(parseInfo.CompilationUnit.ProjectContent); + RazorCompilationUnit compilationUnit = RazorCompilationUnit.CreateFromParseInfo(parseInfo); AddDefaultUsings(compilationUnit); AddWebViewPageClass(compilationUnit); return new ParseInformation(compilationUnit); @@ -58,34 +58,52 @@ namespace ICSharpCode.AspNet.Mvc.Completion return defaultUsing; } - void AddWebViewPageClass(DefaultCompilationUnit compilationUnit) + void AddWebViewPageClass(RazorCompilationUnit compilationUnit) { DefaultClass webViewPageClass = CreateWebViewPageClass(compilationUnit); compilationUnit.Classes.Add(webViewPageClass); } - DefaultClass CreateWebViewPageClass(ICompilationUnit compilationUnit) + DefaultClass CreateWebViewPageClass(RazorCompilationUnit compilationUnit) { var webViewPageClass = new DefaultClass(compilationUnit, "RazorWebViewPage") { Region = new DomRegion(1, 0, 3, 0) }; - AddWebViewPageBaseClass(webViewPageClass); + IReturnType modelType = GetModelReturnType(compilationUnit); + AddWebViewPageBaseClass(webViewPageClass, modelType); return webViewPageClass; } - void AddWebViewPageBaseClass(DefaultClass webViewPageClass) + IReturnType GetModelReturnType(RazorCompilationUnit compilationUnit) + { + IClass modelType = GetClassIfTypeNameIsNotEmpty(compilationUnit.ProjectContent, compilationUnit.ModelTypeName); + if (modelType != null) { + return modelType.DefaultReturnType; + } + return new DynamicReturnType(compilationUnit.ProjectContent); + } + + IClass GetClassIfTypeNameIsNotEmpty(IProjectContent projectContent, string modelTypeName) + { + if (!String.IsNullOrEmpty(modelTypeName)) { + return projectContent.GetClass(modelTypeName, 0); + } + return null; + } + + void AddWebViewPageBaseClass(DefaultClass webViewPageClass, IReturnType modelType) { IClass webViewPageBaseClass = webViewPageClass.ProjectContent.GetClass("System.Web.Mvc.WebViewPage", 1); if (webViewPageBaseClass != null) { - IReturnType returnType = GetWebViewPageBaseClassReturnType(webViewPageBaseClass); + IReturnType returnType = GetWebViewPageBaseClassReturnType(webViewPageBaseClass, modelType); webViewPageClass.BaseTypes.Add(returnType); } } - IReturnType GetWebViewPageBaseClassReturnType(IClass webViewPageBaseClass) + IReturnType GetWebViewPageBaseClassReturnType(IClass webViewPageBaseClass, IReturnType modelType) { var typeArguments = new List(); - typeArguments.Add(new DynamicReturnType(webViewPageBaseClass.ProjectContent)); + typeArguments.Add(modelType); return new ConstructedReturnType(webViewPageBaseClass.DefaultReturnType, typeArguments); } diff --git a/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpModelTypeLocater.cs b/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpModelTypeLocater.cs new file mode 100644 index 0000000000..36c72f8381 --- /dev/null +++ b/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpModelTypeLocater.cs @@ -0,0 +1,34 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Web.Razor; +using ICSharpCode.SharpDevelop; + +namespace ICSharpCode.AspNet.Mvc.Completion +{ + public class RazorCSharpModelTypeLocater + { + public RazorCSharpModelTypeLocater(ITextBuffer textBuffer) + { + ParserResults results = ParseTemplate(textBuffer); + ModelTypeName = GetModelTypeName(results); + } + + ParserResults ParseTemplate(ITextBuffer textBuffer) + { + var host = new RazorEngineHost(new CSharpRazorCodeLanguage()); + var engine = new RazorTemplateEngine(host); + return engine.ParseTemplate(textBuffer.CreateReader()); + } + + string GetModelTypeName(ParserResults results) + { + var visitor = new RazorCSharpParserModelTypeVisitor(); + results.Document.Accept(visitor); + return visitor.ModelTypeName; + } + + public string ModelTypeName { get; private set; } + } +} diff --git a/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpParser.cs b/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpParser.cs index b1a278c26d..1d8d7645eb 100644 --- a/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpParser.cs +++ b/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpParser.cs @@ -39,7 +39,10 @@ namespace ICSharpCode.AspNet.Mvc.Completion public ICompilationUnit Parse(IProjectContent projectContent, string fileName, ITextBuffer fileContent) { - return new DefaultCompilationUnit(projectContent); + var modelTypeLocater = new RazorCSharpModelTypeLocater(fileContent); + return new RazorCompilationUnit(projectContent) { + ModelTypeName = modelTypeLocater.ModelTypeName + }; } public IResolver CreateResolver() diff --git a/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpParserModelTypeVisitor.cs b/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpParserModelTypeVisitor.cs new file mode 100644 index 0000000000..c1424045ca --- /dev/null +++ b/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCSharpParserModelTypeVisitor.cs @@ -0,0 +1,59 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Web.Razor.Parser; +using System.Web.Razor.Parser.SyntaxTree; + +namespace ICSharpCode.AspNet.Mvc.Completion +{ + public class RazorCSharpParserModelTypeVisitor : ParserVisitor + { + bool foundModelTypeName; + + public RazorCSharpParserModelTypeVisitor() + { + ModelTypeName = String.Empty; + } + + public string ModelTypeName { get; private set; } + + public override void VisitSpan(Span span) + { + Console.WriteLine("Span.Kind: " + span.Kind); + Console.WriteLine("Span.GetType(): " + span.GetType().Name); + Console.WriteLine("Span.Content: '" + span.Content + "'"); + + if (foundModelTypeName) + return; + + if (IsModelSpan(span)) { + VisitModelNameSpan(span.Next); + } + } + + bool IsModelSpan(Span span) + { + return span.Content == "model"; + } + + void VisitModelNameSpan(Span span) + { + if (span == null) + return; + + string firstLineOfMarkup = GetFirstLine(span.Content); + ModelTypeName = firstLineOfMarkup.Trim(); + foundModelTypeName = true; + } + + string GetFirstLine(string text) + { + int endOfLineIndex = text.IndexOf('\r'); + if (endOfLineIndex > 0) { + return text.Substring(0, endOfLineIndex); + } + return text; + } + } +} diff --git a/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCompilationUnit.cs b/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCompilationUnit.cs new file mode 100644 index 0000000000..498c929da1 --- /dev/null +++ b/src/AddIns/BackendBindings/AspNet.Mvc/Project/Src/Completion/RazorCompilationUnit.cs @@ -0,0 +1,34 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using ICSharpCode.SharpDevelop.Dom; + +namespace ICSharpCode.AspNet.Mvc.Completion +{ + public class RazorCompilationUnit : DefaultCompilationUnit + { + public RazorCompilationUnit(IProjectContent projectContent) + : base(projectContent) + { + } + + public static RazorCompilationUnit CreateFromParseInfo(ParseInformation parseInformation) + { + return new RazorCompilationUnit(parseInformation.CompilationUnit.ProjectContent) { + ModelTypeName = GetModelTypeName(parseInformation.CompilationUnit) + }; + } + + static string GetModelTypeName(ICompilationUnit compilationUnit) + { + var originalRazorCompilationUnit = compilationUnit as RazorCompilationUnit; + if (originalRazorCompilationUnit != null) { + return originalRazorCompilationUnit.ModelTypeName; + } + return String.Empty; + } + + public string ModelTypeName { get; set; } + } +} diff --git a/src/AddIns/BackendBindings/AspNet.Mvc/Test/AspNet.Mvc.Tests.csproj b/src/AddIns/BackendBindings/AspNet.Mvc/Test/AspNet.Mvc.Tests.csproj index e4487ba0af..f0bf407d37 100644 --- a/src/AddIns/BackendBindings/AspNet.Mvc/Test/AspNet.Mvc.Tests.csproj +++ b/src/AddIns/BackendBindings/AspNet.Mvc/Test/AspNet.Mvc.Tests.csproj @@ -117,6 +117,7 @@ + @@ -189,6 +190,7 @@ + \ No newline at end of file diff --git a/src/AddIns/BackendBindings/AspNet.Mvc/Test/Src/Completion/RazorCSharpParserTests.cs b/src/AddIns/BackendBindings/AspNet.Mvc/Test/Src/Completion/RazorCSharpParserTests.cs new file mode 100644 index 0000000000..0ec664c234 --- /dev/null +++ b/src/AddIns/BackendBindings/AspNet.Mvc/Test/Src/Completion/RazorCSharpParserTests.cs @@ -0,0 +1,148 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using ICSharpCode.AspNet.Mvc.Completion; +using ICSharpCode.SharpDevelop; +using ICSharpCode.SharpDevelop.Dom; +using NUnit.Framework; + +namespace AspNet.Mvc.Tests.Completion +{ + [TestFixture] + public class RazorCSharpParserTests + { + RazorCSharpParser parser; + + void CreateParser() + { + parser = new RazorCSharpParser(); + } + + ICompilationUnit Parse(string code) + { + var projectContent = new DefaultProjectContent(); + var textBuffer = new StringTextBuffer(code); + return parser.Parse(projectContent, @"d:\MyProject\Views\Index.cshtml", textBuffer); + } + + [Test] + public void Parse_ModelDirectiveWithTypeName_ModelTypeNameFound() + { + CreateParser(); + string code = "@model MvcApplication.MyModel\r\n"; + + var compilationUnit = Parse(code) as RazorCompilationUnit; + + Assert.AreEqual("MvcApplication.MyModel", compilationUnit.ModelTypeName); + } + + [Test] + public void Parse_ModelDirectiveWithTypeNameFollowedByHtmlMarkup_ModelTypeNameFound() + { + CreateParser(); + string code = + "@model MvcApplication.LogonModel\r\n" + + "

Index

\r\n"; + + var compilationUnit = Parse(code) as RazorCompilationUnit; + + Assert.AreEqual("MvcApplication.LogonModel", compilationUnit.ModelTypeName); + } + + [Test] + public void Parse_SingleLineFileWithModelDirectiveAndTypeNameButNoNewLineAtEnd_ModelTypeNameFound() + { + CreateParser(); + string code = "@model MvcApplication.MyModel"; + + var compilationUnit = Parse(code) as RazorCompilationUnit; + + Assert.AreEqual("MvcApplication.MyModel", compilationUnit.ModelTypeName); + } + + [Test] + public void Parse_ModelTypeDirectiveWithTypeNameFollowedByRazorBlock_ModelTypeNameFound() + { + CreateParser(); + + string code = + "@model IEnumerable\r\n" + + "\r\n" + + "@{\r\n" + + " ViewBag.Title = \"Title1\";\r\n" + + "}\r\n" + + "\r\n"; + + var compilationUnit = Parse(code) as RazorCompilationUnit; + + Assert.AreEqual("IEnumerable", compilationUnit.ModelTypeName); + } + + [Test] + public void Parse_UsingDirective_ModelTypeNameIsEmptyString() + { + CreateParser(); + string code = "@using System.Xml\r\n"; + + var compilationUnit = Parse(code) as RazorCompilationUnit; + + Assert.AreEqual(String.Empty, compilationUnit.ModelTypeName); + } + + [Test] + public void Parse_HelperDirective_ModelTypeNameIsEmptyString() + { + CreateParser(); + string code = "@helper MyHelper\r\n"; + + var compilationUnit = Parse(code) as RazorCompilationUnit; + + Assert.AreEqual(String.Empty, compilationUnit.ModelTypeName); + } + + [Test] + public void Parse_HtmlMarkupOnly_ModelTypeNameIsEmptyString() + { + CreateParser(); + string code = "

heading

\r\n"; + + var compilationUnit = Parse(code) as RazorCompilationUnit; + + Assert.AreEqual(String.Empty, compilationUnit.ModelTypeName); + } + + [Test] + public void Parse_ModelDirectiveOnly_ModelTypeNameIsEmptyString() + { + CreateParser(); + string code = "@model"; + + var compilationUnit = Parse(code) as RazorCompilationUnit; + + Assert.AreEqual(String.Empty, compilationUnit.ModelTypeName); + } + + [Test] + public void Parse_ModelStringInsideParagraphTags_ModelTypeNameIsEmptyString() + { + CreateParser(); + string code = "

model

"; + + var compilationUnit = Parse(code) as RazorCompilationUnit; + + Assert.AreEqual(String.Empty, compilationUnit.ModelTypeName); + } + + [Test] + public void Parse_ModelStringOnlyWithoutRazorTransition_ModelTypeNameIsEmptyString() + { + CreateParser(); + string code = "model"; + + var compilationUnit = Parse(code) as RazorCompilationUnit; + + Assert.AreEqual(String.Empty, compilationUnit.ModelTypeName); + } + } +}