From 50314ca7e56b32500ec1d732edb34bceee5b1cd9 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Mon, 17 Mar 2014 15:35:17 -0700 Subject: [PATCH] Add the ability for users to await expressions. This enabled things like @await Foo(). We special case the await keyword and allow another snippet of code to follow it. --- .../Parser/CSharpCodeParser.Expressions.cs | 41 +++++++++++++++++++ .../Parser/CSharpCodeParser.cs | 26 +++++++++--- .../Tokenizer/CSharpKeywordDetector.cs | 1 + .../Tokenizer/Symbols/CSharpKeyword.cs | 1 + 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Expressions.cs diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Expressions.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Expressions.cs new file mode 100644 index 0000000000..cad84a614e --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Expressions.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public partial class CSharpCodeParser + { + private void SetUpExpressions() + { + MapKeywords(AwaitExpression, CSharpKeyword.Await); + } + + private void AwaitExpression(bool topLevel) + { + // Ensure that we're on the await statement (only runs in debug) + Assert(CSharpKeyword.Await); + + // Accept the "await" and move on + AcceptAndMoveNext(); + + // Accept 1 or more spaces between the await and the following code. + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + // Accept a single code piece to await. This will accept up until a method "call" signature. + // Ex: "@await |Foo|()" Inbetween the pipes is what is accepted. The Statement/ImplicitExpression + // handling capture method calls and the parameters passed in. + AcceptWhile(CSharpSymbolType.Identifier); + + // Top level basically indicates if we're within an expression or statement. + // Ex: topLevel true = @await Foo() | topLevel false = @{ await Foo(); } + // Note that in this case @{ @await Foo() } top level is true for await. + // Therefore, if we're top level then we want to act like an implicit expression, + // otherwise just act as whatever we're contained in. + if (topLevel) + { + // Setup the Span to be an async implicit expression (an implicit expresison that allows spaces). + // Spaces are allowed because of "@await Foo()". + AsyncImplicitExpression(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs index 07a8c721c4..1a6493fd92 100644 --- a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNet.Razor.Parser internal static ISet DefaultKeywords = new HashSet() { + "await", "if", "do", "try", @@ -45,6 +46,7 @@ namespace Microsoft.AspNet.Razor.Parser Keywords = new HashSet(); SetUpKeywords(); SetupDirectives(); + SetUpExpressions(); } protected internal ISet Keywords { get; private set; } @@ -307,6 +309,18 @@ namespace Microsoft.AspNet.Razor.Parser } private void ImplicitExpression() + { + ImplicitExpression(AcceptedCharacters.NonWhiteSpace); + } + + // Async implicit expressions include the "await" keyword and therefore need to allow spaces to + // separate the "await" and the following code. + private void AsyncImplicitExpression() + { + ImplicitExpression(AcceptedCharacters.AnyExceptNewline); + } + + private void ImplicitExpression(AcceptedCharacters acceptedCharacters) { Context.CurrentBlock.Type = BlockType.Expression; Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator(); @@ -314,7 +328,7 @@ namespace Microsoft.AspNet.Razor.Parser using (PushSpanConfig(span => { span.EditHandler = new ImplicitExpressionEditHandler(Language.TokenizeString, Keywords, acceptTrailingDot: IsNested); - span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace; + span.EditHandler.AcceptedCharacters = acceptedCharacters; span.CodeGenerator = new ExpressionCodeGenerator(); })) { @@ -325,14 +339,14 @@ namespace Microsoft.AspNet.Razor.Parser AcceptAndMoveNext(); } } - while (MethodCallOrArrayIndex()); + while (MethodCallOrArrayIndex(acceptedCharacters)); PutCurrentBack(); Output(SpanKind.Code); } } - private bool MethodCallOrArrayIndex() + private bool MethodCallOrArrayIndex(AcceptedCharacters acceptedCharacters) { if (!EndOfFile) { @@ -361,9 +375,11 @@ namespace Microsoft.AspNet.Razor.Parser if (At(right)) { AcceptAndMoveNext(); - Span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace; + + // At the ending brace, restore the initial accepted characters. + Span.EditHandler.AcceptedCharacters = acceptedCharacters; } - return MethodCallOrArrayIndex(); + return MethodCallOrArrayIndex(acceptedCharacters); } if (CurrentSymbol.Type == CSharpSymbolType.Dot) { diff --git a/src/Microsoft.AspNet.Razor/Tokenizer/CSharpKeywordDetector.cs b/src/Microsoft.AspNet.Razor/Tokenizer/CSharpKeywordDetector.cs index 24dc4ca607..fcab8c06fa 100644 --- a/src/Microsoft.AspNet.Razor/Tokenizer/CSharpKeywordDetector.cs +++ b/src/Microsoft.AspNet.Razor/Tokenizer/CSharpKeywordDetector.cs @@ -10,6 +10,7 @@ namespace Microsoft.AspNet.Razor.Tokenizer { private static readonly Dictionary _keywords = new Dictionary(StringComparer.Ordinal) { + { "await", CSharpKeyword.Await }, { "abstract", CSharpKeyword.Abstract }, { "byte", CSharpKeyword.Byte }, { "class", CSharpKeyword.Class }, diff --git a/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/CSharpKeyword.cs b/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/CSharpKeyword.cs index a16b7c6c90..607df73f66 100644 --- a/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/CSharpKeyword.cs +++ b/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/CSharpKeyword.cs @@ -4,6 +4,7 @@ namespace Microsoft.AspNet.Razor.Tokenizer.Symbols { public enum CSharpKeyword { + Await, Abstract, Byte, Class,