diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/AsyncKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/AsyncKeywordRecommenderTests.cs index 8c112bb88e47fa71315c89ff7df6842c077dfa8e..5a321ba27fb903e43e39b7e5b1362abb2ec5014f 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/AsyncKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/AsyncKeywordRecommenderTests.cs @@ -128,6 +128,120 @@ public async Task TestNotAfterPartialInClass() class Foo { partial $$ +}"); + } + + [Fact] + [WorkItem(8616, "https://github.com/dotnet/roslyn/issues/8616")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + public async Task TestLocalFunction() + { + await VerifyKeywordAsync(@" +class Foo +{ + public void M() + { + $$ + } +}"); + } + + [Fact] + [WorkItem(14525, "https://github.com/dotnet/roslyn/issues/14525")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction2() + { + await VerifyKeywordAsync(@" +class Foo +{ + public void M() + { + unsafe $$ + } +}"); + } + + [Fact] + [WorkItem(14525, "https://github.com/dotnet/roslyn/issues/14525")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction3() + { + await VerifyKeywordAsync(@" +class Foo +{ + public void M() + { + unsafe $$ void L() { } + } +}"); + } + + [Fact] + [WorkItem(8616, "https://github.com/dotnet/roslyn/issues/8616")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction4() + { + await VerifyKeywordAsync(@" +class Foo +{ + public void M() + { + $$ void L() { } + } +}"); + } + + [Fact] + [WorkItem(8616, "https://github.com/dotnet/roslyn/issues/8616")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction5() + { + await VerifyKeywordAsync(@" +class Foo +{ + public void M(Action a) + { + M(async () => + { + $$ + }); + } +}"); + } + + [Fact] + [WorkItem(8616, "https://github.com/dotnet/roslyn/issues/8616")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction6() + { + await VerifyAbsenceAsync(@" +class Foo +{ + public void M() + { + int $$ + } +}"); + } + + [Fact] + [WorkItem(8616, "https://github.com/dotnet/roslyn/issues/8616")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction7() + { + await VerifyAbsenceAsync(@" +class Foo +{ + public void M() + { + static $$ + } }"); } } diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs index 0f96afeb59ff846d2ef1402d29124e12832d4392..e521a19b61862e5e5bfba557d84859efba54f520 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs @@ -83,13 +83,6 @@ public async Task TestNotInCastType2() @"var str = (($$)items) as string;")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] - public async Task TestNotInEmptyStatement() - { - await VerifyAbsenceAsync(AddInsideMethod( -@"$$")); - } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] public async Task TestInTypeOf() { @@ -642,5 +635,123 @@ public async Task TestNotAfterAsyncAsType() { await VerifyAbsenceAsync(@"class c { async async $$ }"); } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction() + { + await VerifyKeywordAsync(@" +class C +{ + void M() + { + $$ + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [WorkItem(14525, "https://github.com/dotnet/roslyn/issues/14525")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction2() + { + await VerifyKeywordAsync(@" +class C +{ + void M() + { + async $$ + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction3() + { + await VerifyAbsenceAsync(@" +class C +{ + void M() + { + async async $$ + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction4() + { + await VerifyAbsenceAsync(@" +class C +{ + void M() + { + var async $$ + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction5() + { + await VerifyAbsenceAsync(@" +using System; +class C +{ + void M(Action a) + { + M(async $$ () => + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction6() + { + await VerifyKeywordAsync(@" +class C +{ + void M() + { + unsafe async $$ + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction7() + { + await VerifyKeywordAsync(@" +using System; +class C +{ + void M(Action a) + { + M(async () => + { + async $$ + }) + } +}"); + } } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs index 7b20dcdf7e9aec116327803b010517ad3a2cac6f..cd4179ae33f2af2b5807837c94175b1c21462aae 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs @@ -21,8 +21,13 @@ protected override bool IsValidContext(int position, CSharpSyntaxContext context return true; } - return !context.TargetToken.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword) - && InMemberDeclarationContext(position, context, cancellationToken); + if (context.TargetToken.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword)) + { + return false; + } + + return InMemberDeclarationContext(position, context, cancellationToken) + || context.SyntaxTree.IsLocalFunctionDeclarationContext(position, cancellationToken); } private static bool InMemberDeclarationContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs index a78636245a1044ce3465fe4c24d41dc7a6c83bcc..079c0cd9ffcca43dc328b85303961d544eb0608d 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs @@ -45,7 +45,8 @@ protected override bool IsValidContext(int position, CSharpSyntaxContext context IsUnsafeParameterTypeContext(context) || IsUnsafeCastTypeContext(context) || IsUnsafeDefaultExpressionContext(context, cancellationToken) || - context.IsFixedVariableDeclarationContext; + context.IsFixedVariableDeclarationContext || + context.SyntaxTree.IsLocalFunctionDeclarationContext(position, cancellationToken); } private bool IsUnsafeDefaultExpressionContext(CSharpSyntaxContext context, CancellationToken cancellationToken) diff --git a/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs b/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs index b53c2a9ca55ecb46573d8e42b9d1ec3b1480e138..bb2317df6ea42f97db3b8aa57c03e6c6422984d0 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs @@ -292,6 +292,46 @@ public static bool IsAttributeNameContext(this SyntaxTree syntaxTree, int positi return false; } + public static bool IsLocalFunctionDeclarationContext( + this SyntaxTree syntaxTree, + int position, + CancellationToken cancellationToken) + { + var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); + var token = leftToken.GetPreviousTokenIfTouchingWord(position); + + // Local functions are always valid in a statement context + if (syntaxTree.IsStatementContext(position, leftToken, cancellationToken)) + { + return true; + } + + // Also valid after certain modifiers + var validModifiers = SyntaxKindSet.LocalFunctionModifiers; + + var modifierTokens = syntaxTree.GetPrecedingModifiers( + position, token, out int beforeModifiersPosition); + + if (modifierTokens.IsSubsetOf(validModifiers)) + { + if (token.HasMatchingText(SyntaxKind.AsyncKeyword)) + { + // second appearance of "async" not followed by modifier: treat as type + if (syntaxTree.GetPrecedingModifiers(token.SpanStart, token, cancellationToken) + .Contains(SyntaxKind.AsyncKeyword)) + { + return false; + } + } + + leftToken = syntaxTree.FindTokenOnLeftOfPosition(beforeModifiersPosition, cancellationToken); + token = leftToken.GetPreviousTokenIfTouchingWord(beforeModifiersPosition); + return syntaxTree.IsStatementContext(beforeModifiersPosition, token, cancellationToken); + } + + return false; + } + public static bool IsTypeDeclarationContext( this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken) { diff --git a/src/Workspaces/CSharp/Portable/Extensions/SyntaxTreeExtensions.cs b/src/Workspaces/CSharp/Portable/Extensions/SyntaxTreeExtensions.cs index d7b5bbf0fc257b9a5f524cba8d0b66a5f453981a..3b18d66726392319992a9248820015fbe5108375 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/SyntaxTreeExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/SyntaxTreeExtensions.cs @@ -16,7 +16,17 @@ namespace Microsoft.CodeAnalysis.CSharp.Extensions internal static partial class SyntaxTreeExtensions { public static ISet GetPrecedingModifiers( - this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken) + this SyntaxTree syntaxTree, + int position, + SyntaxToken tokenOnLeftOfPosition, + CancellationToken cancellationToken) + => syntaxTree.GetPrecedingModifiers(position, tokenOnLeftOfPosition, out var _); + + public static ISet GetPrecedingModifiers( + this SyntaxTree syntaxTree, + int position, + SyntaxToken tokenOnLeftOfPosition, + out int positionBeforeModifiers) { var token = tokenOnLeftOfPosition; token = token.GetPreviousTokenIfTouchingWord(position); @@ -58,6 +68,7 @@ internal static partial class SyntaxTreeExtensions break; } + positionBeforeModifiers = token.FullSpan.End; return result; } diff --git a/src/Workspaces/CSharp/Portable/Utilities/SyntaxKindSet.cs b/src/Workspaces/CSharp/Portable/Utilities/SyntaxKindSet.cs index 6504971282790f902abbe8655954b69fb2c5f32d..4207b933c72cd87503ddaae7d1695d0c0bac0afd 100644 --- a/src/Workspaces/CSharp/Portable/Utilities/SyntaxKindSet.cs +++ b/src/Workspaces/CSharp/Portable/Utilities/SyntaxKindSet.cs @@ -52,6 +52,12 @@ internal class SyntaxKindSet SyntaxKind.VolatileKeyword, }; + public static readonly ISet LocalFunctionModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.AsyncKeyword, + SyntaxKind.UnsafeKeyword + }; + public static readonly ISet AllTypeDeclarations = new HashSet(SyntaxFacts.EqualityComparer) { SyntaxKind.InterfaceDeclaration,