From 2a5025d3d3930bb358125f7c26adacd2d4b106d0 Mon Sep 17 00:00:00 2001 From: Andrew Hall Date: Wed, 30 Nov 2022 17:03:31 -0800 Subject: [PATCH] Revert "Revert "Merge pull request #65407 from Youssef1313/scoped-ide" (#65647)" This reverts commit 17665157eff63d99d6e02b3896286a02b8efe012. --- .../Classification/TotalClassifierTests.cs | 39 ++ .../SymbolCompletionProviderTests.cs | 59 ++- .../CSharpReassignedVariableTests.cs | 52 +++ .../GlobalKeywordRecommenderTests.cs | 7 + .../InKeywordRecommenderTests.cs | 198 ++++---- .../OutKeywordRecommenderTests.cs | 36 ++ .../RefKeywordRecommenderTests.cs | 49 ++ .../ScopedKeywordRecommenderTests.cs | 435 ++++++++++++++++++ .../VarKeywordRecommenderTests.cs | 7 + .../CSharpCompletionCommandHandlerTests.vb | 102 ++++ .../GlobalKeywordRecommender.cs | 6 +- .../InKeywordRecommender.cs | 2 +- .../OutKeywordRecommender.cs | 2 +- .../RefKeywordRecommender.cs | 7 +- .../ScopedKeywordRecommender.cs | 56 +++ .../NameSyntaxClassifier.cs | 1 + .../Extensions/ExpressionSyntaxExtensions.cs | 13 +- .../ContextQuery/SyntaxTreeExtensions.cs | 56 ++- 18 files changed, 1026 insertions(+), 101 deletions(-) create mode 100644 src/EditorFeatures/CSharpTest2/Recommendations/ScopedKeywordRecommenderTests.cs create mode 100644 src/Features/CSharp/Portable/Completion/KeywordRecommenders/ScopedKeywordRecommender.cs diff --git a/src/EditorFeatures/CSharpTest/Classification/TotalClassifierTests.cs b/src/EditorFeatures/CSharpTest/Classification/TotalClassifierTests.cs index b31bfc10b4df4..62ecd91e8d45f 100644 --- a/src/EditorFeatures/CSharpTest/Classification/TotalClassifierTests.cs +++ b/src/EditorFeatures/CSharpTest/Classification/TotalClassifierTests.cs @@ -2617,6 +2617,45 @@ static void staticLocalFunction() { }|] Punctuation.CloseCurly); } + [Theory] + [CombinatorialData] + public async Task TestScopedVar(TestHost testHost) + { + await TestAsync(""" + static void method(scoped in S s) + { + scoped var rs1 = s; + } + + file readonly ref struct S { } + """, testHost, + Keyword("static"), + Keyword("void"), + Method("method"), + Static("method"), + Punctuation.OpenParen, + Keyword("scoped"), + Keyword("in"), + Struct("S"), + Parameter("s"), + Punctuation.CloseParen, + Punctuation.OpenCurly, + Keyword("scoped"), + Keyword("var"), + Local("rs1"), + Operators.Equals, + Parameter("s"), + Punctuation.Semicolon, + Punctuation.CloseCurly, + Keyword("file"), + Keyword("readonly"), + Keyword("ref"), + Keyword("struct"), + Struct("S"), + Punctuation.OpenCurly, + Punctuation.CloseCurly); + } + [Theory] [CombinatorialData] public async Task Lambda_DefaultParameterValue(TestHost testHost) diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs index 11660b2e61cef..236eac88b045a 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.CSharp; @@ -11964,11 +11965,67 @@ class C await VerifyAnyItemExistsAsync(source); } + [Fact] + public async Task AfterScopedInsideMethod() + { + var source = @" +class C +{ + void M() + { + scoped $$ + } +} + +ref struct MyRefStruct { } +"; + await VerifyItemExistsAsync(MakeMarkup(source), "MyRefStruct"); + } + + [Fact] + public async Task AfterScopedGlobalStatement_FollowedByType() + { + var source = @" +scoped $$ + +ref struct MyRefStruct { } +"; + await VerifyItemExistsAsync(MakeMarkup(source), "MyRefStruct"); + } + + [Fact] + public async Task AfterScopedGlobalStatement_NotFollowedByType() + { + var source = """ + using System; + + scoped $$ + """; + + await VerifyItemExistsAsync(MakeMarkup(source), "ReadOnlySpan", displayTextSuffix: "<>"); + } + + [Fact] + public async Task AfterScopedInParameter() + { + var source = @" +class C +{ + void M(scoped $$) + { + } +} + +ref struct MyRefStruct { } +"; + await VerifyItemExistsAsync(MakeMarkup(source), "MyRefStruct"); + } + private static string MakeMarkup(string source, string languageVersion = "Preview") { return $$""" - + {{source}} diff --git a/src/EditorFeatures/CSharpTest/ReassignedVariable/CSharpReassignedVariableTests.cs b/src/EditorFeatures/CSharpTest/ReassignedVariable/CSharpReassignedVariableTests.cs index f7fe02f04df99..5bac067b28fb6 100644 --- a/src/EditorFeatures/CSharpTest/ReassignedVariable/CSharpReassignedVariableTests.cs +++ b/src/EditorFeatures/CSharpTest/ReassignedVariable/CSharpReassignedVariableTests.cs @@ -510,6 +510,24 @@ void M(ref int [|p|]) }"); } + [Fact] + public async Task AssignmentThroughScopedRefLocal() + { + await TestAsync( +@" +using System; +class C +{ + void M(ref int [|p|]) + { + scoped ref var [|local|] = ref [|p|]; + [|local|] = 0; + [|local|] = 1; + Console.WriteLine([|local|]); + } +}"); + } + [Fact] public async Task TestRefLocalReassignment() { @@ -653,6 +671,23 @@ void M() }"); } + [Fact] + public async Task TestScopedReadonlyRefLocalWithNoReassignment() + { + await TestAsync( +@" +using System; +class C +{ + void M() + { + int p = 0; + scoped ref readonly int refP = ref p; + Console.WriteLine(p); + } +}"); + } + [Fact] public async Task TestReadonlyRefLocalWithNoReassignment1() { @@ -670,6 +705,23 @@ void M() }"); } + [Fact] + public async Task TestScopedReadonlyRefLocalWithNoReassignment1() + { + await TestAsync( +@" +using System; +class C +{ + void M1() + { + int p = 0; + scoped ref readonly int refP = ref p!; + Console.WriteLine(p); + } +}"); + } + [Fact] public async Task TestPointerCausingPossibleReassignment() { diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/GlobalKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/GlobalKeywordRecommenderTests.cs index e7f2b6d073a5e..f3c046bc6a1e8 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/GlobalKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/GlobalKeywordRecommenderTests.cs @@ -349,5 +349,12 @@ await VerifyKeywordAsync( @"$$ [assembly: Call()]"); } + + [Fact] + public async Task TestAfterScoped() + { + await VerifyKeywordAsync("scoped $$"); + await VerifyKeywordAsync(AddInsideMethod("scoped $$")); + } } } diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/InKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/InKeywordRecommenderTests.cs index 0115856792b42..f0337f6b80b3b 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/InKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/InKeywordRecommenderTests.cs @@ -9,16 +9,17 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Recommendations { + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] public class InKeywordRecommenderTests : KeywordRecommenderTests { - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotAtRoot_Interactive() { await VerifyAbsenceAsync(SourceCodeKind.Script, @"$$"); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotAfterClass_Interactive() { await VerifyAbsenceAsync(SourceCodeKind.Script, @@ -26,7 +27,7 @@ await VerifyAbsenceAsync(SourceCodeKind.Script, $$"); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotAfterGlobalStatement_Interactive() { await VerifyAbsenceAsync(SourceCodeKind.Script, @@ -34,7 +35,7 @@ await VerifyAbsenceAsync(SourceCodeKind.Script, $$"); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotAfterGlobalVariableDeclaration_Interactive() { await VerifyAbsenceAsync(SourceCodeKind.Script, @@ -42,49 +43,49 @@ await VerifyAbsenceAsync(SourceCodeKind.Script, $$"); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotInUsingAlias() { await VerifyAbsenceAsync( @"using Goo = $$"); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotInGlobalUsingAlias() { await VerifyAbsenceAsync( @"global using Goo = $$"); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotInEmptyStatement() { await VerifyAbsenceAsync(AddInsideMethod( @"$$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotAfterFrom() { await VerifyAbsenceAsync(AddInsideMethod( @"var q = from $$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestAfterFromIdentifier() { await VerifyKeywordAsync(AddInsideMethod( @"var q = from x $$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestAfterFromAndTypeAndIdentifier() { await VerifyKeywordAsync(AddInsideMethod( @"var q = from int x $$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotAfterJoin() { await VerifyAbsenceAsync(AddInsideMethod( @@ -92,7 +93,7 @@ await VerifyAbsenceAsync(AddInsideMethod( join $$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestAfterJoinIdentifier() { await VerifyKeywordAsync(AddInsideMethod( @@ -100,7 +101,7 @@ await VerifyKeywordAsync(AddInsideMethod( join z $$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestAfterJoinAndTypeAndIdentifier() { await VerifyKeywordAsync(AddInsideMethod( @@ -108,7 +109,7 @@ await VerifyKeywordAsync(AddInsideMethod( join int z $$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestAfterJoinNotAfterIn() { await VerifyAbsenceAsync(AddInsideMethod( @@ -116,7 +117,7 @@ await VerifyAbsenceAsync(AddInsideMethod( join z in $$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] [WorkItem(544158, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/544158")] public async Task TestNotAfterJoinPredefinedType() { @@ -130,7 +131,7 @@ void M() join int $$"); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] [WorkItem(544158, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/544158")] public async Task TestNotAfterJoinType() { @@ -144,126 +145,126 @@ void M() join Int32 $$"); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestInForEach() { await VerifyKeywordAsync(AddInsideMethod( @"foreach (var v $$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestInForEach1() { await VerifyKeywordAsync(AddInsideMethod( @"foreach (var v $$ c")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestInForEach2() { await VerifyKeywordAsync(AddInsideMethod( @"foreach (var v $$ c")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotInForEach() { await VerifyAbsenceAsync(AddInsideMethod( @"foreach ($$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotInForEach1() { await VerifyAbsenceAsync(AddInsideMethod( @"foreach (var $$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotInForEach2() { await VerifyAbsenceAsync(AddInsideMethod( @"foreach (var v in $$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestNotInForEach3() { await VerifyAbsenceAsync(AddInsideMethod( @"foreach (var v in c $$")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestInterfaceTypeVarianceAfterAngle() { await VerifyKeywordAsync( @"interface IGoo<$$"); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [Fact] public async Task TestInterfaceTypeVarianceNotAfterIn() { await VerifyAbsenceAsync( @"interface IGoo "scoped"; + + private readonly ScopedKeywordRecommender _recommender = new(); + + public ScopedKeywordRecommenderTests() + { + this.RecommendKeywordsAsync = (position, context) => Task.FromResult(_recommender.RecommendKeywords(position, context, CancellationToken.None)); + } + + [Fact] + public async Task TestAtRoot_Interactive() + { + await VerifyKeywordAsync(SourceCodeKind.Script, +@"$$"); + } + + [Fact] + public async Task TestAfterClass_Interactive() + { + await VerifyKeywordAsync(SourceCodeKind.Script, +@"class C { } +$$"); + } + + [Fact] + public async Task TestAfterGlobalStatement_Interactive() + { + await VerifyKeywordAsync(SourceCodeKind.Script, +@"System.Console.WriteLine(); +$$"); + } + + [Fact] + public async Task TestAfterGlobalVariableDeclaration_Interactive() + { + await VerifyKeywordAsync(SourceCodeKind.Script, +@"int i = 0; +$$"); + } + + [Fact] + public async Task TestNotInUsingAlias() + { + await VerifyAbsenceAsync( +@"using Goo = $$"); + } + + [Fact] + public async Task TestNotInGlobalUsingAlias() + { + await VerifyAbsenceAsync( +@"global using Goo = $$"); + } + + [Fact] + public async Task TestNotAfterStackAlloc() + { + await VerifyAbsenceAsync( +@"class C { + int* goo = stackalloc $$"); + } + + [Fact] + public async Task TestNotInFixedStatement() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"fixed ($$")); + } + + [Fact] + public async Task TestNotInDelegateReturnType() + { + await VerifyAbsenceAsync( +@"public delegate $$"); + } + + [Fact] + public async Task TestPossibleLambda() + { + // Could be `var x = ((scoped ref int x) => x);` + await VerifyKeywordAsync(AddInsideMethod( +@"var x = (($$")); + } + + [Fact] + public async Task TestEmptyStatement() + { + await VerifyKeywordAsync(AddInsideMethod( +@"$$")); + } + + [Fact] + public async Task TestBeforeStatement() + { + await VerifyKeywordAsync(AddInsideMethod( +@"$$ +return true;")); + } + + [Fact] + public async Task TestAfterStatement() + { + await VerifyKeywordAsync(AddInsideMethod( +@"return true; +$$")); + } + + [Fact] + public async Task TestNotInClass() + { + await VerifyAbsenceAsync(@"class C +{ + $$ +}"); + } + + [Fact] + public async Task TestInFor() + { + await VerifyKeywordAsync(AddInsideMethod( +@"for ($$")); + } + + [Fact] + public async Task TestInFor2() + { + await VerifyKeywordAsync(AddInsideMethod( +@"for ($$;")); + } + + [Fact] + public async Task TestInFor3() + { + await VerifyKeywordAsync(AddInsideMethod( +@"for ($$;;")); + } + + [Fact] + public async Task TestNotInFor() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"for (var $$")); + } + + [Fact] + public async Task TestNotAfterVar() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"var $$")); + } + + [Fact] + public async Task TestInForEach() + { + await VerifyKeywordAsync(AddInsideMethod( +@"foreach ($$")); + } + + [Fact] + public async Task TestNotInForEach() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"foreach (var $$")); + } + + [Fact] + public async Task TestInAwaitForEach() + { + await VerifyKeywordAsync(AddInsideMethod( +@"await foreach ($$")); + } + + [Fact] + public async Task TestNotInAwaitForEach() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"await foreach (var $$")); + } + + [Fact] + public async Task TestNotInForEachRefLoop0() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"foreach (ref $$")); + } + + [Fact] + public async Task TestNotInForEachRefLoop1() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"foreach (ref $$ x")); + } + + [Fact] + public async Task TestNotInForEachRefLoop2() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"foreach (ref s$$ x")); + } + + [Fact] + public async Task TestNotInForEachRefReadonlyLoop0() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"foreach (ref readonly $$ x")); + } + + [Fact] + public async Task TestNotInForRefLoop0() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"for (ref $$")); + } + + [Fact] + public async Task TestNotInForRefLoop1() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"for (ref s$$")); + } + + [Fact] + public async Task TestNotInForRefReadonlyLoop0() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"for (ref readonly $$")); + } + + [Fact] + public async Task TestNotInForRefReadonlyLoop1() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"for (ref readonly s$$")); + } + + [Fact] + public async Task TestNotInUsing() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"using ($$")); + } + + [Fact] + public async Task TestNotInUsing2() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"using (var $$")); + } + + [Fact] + public async Task TestNotInAwaitUsing() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"await using ($$")); + } + + [Fact] + public async Task TestNotInAwaitUsing2() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"await using (var $$")); + } + + [Fact] + public async Task TestNotAfterConstLocal() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"const $$")); + } + + [Fact] + public async Task TestNotAfterConstField() + { + await VerifyAbsenceAsync( +@"class C { + const $$"); + } + + [Fact] + public async Task TestAfterOutKeywordInArgument() + { + await VerifyKeywordAsync(AddInsideMethod( +@"M(out $$")); + } + + [Fact] + public async Task TestNotAfterRefInMemberContext() + { + await VerifyAbsenceAsync( +@"class C { + ref $$"); + } + + [Fact] + public async Task TestNotAfterRefReadonlyInMemberContext() + { + await VerifyAbsenceAsync( +@"class C { + ref readonly $$"); + } + + [Fact] + public async Task TestNotAfterRefInStatementContext() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"ref $$")); + } + + [Fact] + public async Task TestNotAfterRefReadonlyInStatementContext() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"ref readonly $$")); + } + + [Fact] + public async Task TestNotAfterRefLocalDeclaration() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"ref $$ int local;")); + } + + [Fact] + public async Task TestNotAfterRefReadonlyLocalDeclaration() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"ref readonly $$ int local;")); + } + + [Fact] + public async Task TestNotAfterRefExpression() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"ref int x = ref $$")); + } + + [Fact] + public async Task TestInParameter1() + { + await VerifyKeywordAsync(""" + class C + { + public void M($$) + } + """); + } + + [Fact] + public async Task TestInParameter2() + { + await VerifyKeywordAsync(""" + class C + { + public void M($$ ref) + } + """); + } + + [Fact] + public async Task TestInParameter3() + { + await VerifyKeywordAsync(""" + class C + { + public void M($$ ref int i) + } + """); + } + + [Fact] + public async Task TestInParameter4() + { + await VerifyKeywordAsync(""" + class C + { + public void M($$ ref int i) + } + """); + } + + [Fact] + public async Task TestInOperatorParameter() + { + await VerifyKeywordAsync(""" + class C + { + public static C operator +($$ in C c) + } + """); + } + + [Fact] + public async Task TestInAnonymousMethodParameter() + { + await VerifyKeywordAsync(""" + class C + { + void M() + { + var x = delegate ($$) { }; + } + } + """); + } + + [Fact] + public async Task TestInParameterAfterThisScoped() + { + await VerifyKeywordAsync(""" + static class C + { + static void M(this $$) + } + """); + } + } +} diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/VarKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/VarKeywordRecommenderTests.cs index 9db736fd230b7..cbb86f9066ffb 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/VarKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/VarKeywordRecommenderTests.cs @@ -450,5 +450,12 @@ public async Task TestInMixedDeclarationAndAssignmentInDeconstruction() await VerifyKeywordAsync(AddInsideMethod( @"(x, $$) = (0, 0);")); } + + [Fact] + public async Task TestAfterScoped() + { + await VerifyKeywordAsync(AddInsideMethod("scoped $$")); + await VerifyKeywordAsync("scoped $$"); + } } } diff --git a/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb b/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb index 38cbcb9eb9e22..ff543c0efbd38 100644 --- a/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb +++ b/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb @@ -8885,6 +8885,108 @@ public class AA End Using End Function + + Public Async Function TestTypeImportCompletionAfterScoped(showCompletionInArgumentLists As Boolean) As Task + Using state = TestStateFactory.CreateCSharpTestState( + +namespace MyNamespace +{ + public ref struct MyRefStruct { } +} + +namespace Test +{ + class Program + { + public static void Main() + { + scoped $$ + } + } +} +, + showCompletionInArgumentLists:=showCompletionInArgumentLists) + + state.Workspace.GlobalOptions.SetGlobalOption(New OptionKey(CompletionOptionsStorage.ForceExpandedCompletionIndexCreation), True) + state.Workspace.GlobalOptions.SetGlobalOption( + New OptionKey(CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp), True) + + state.SendInvokeCompletionList() + Await state.WaitForAsynchronousOperationsAsync() + Await state.WaitForUIRenderedAsync() + + ' Make sure expander is selected + Await state.SetCompletionItemExpanderStateAndWaitForUiRenderAsync(isSelected:=True) + + Dim expectedText = " +using MyNamespace; + +namespace MyNamespace +{ + public ref struct MyRefStruct { } +} + +namespace Test +{ + class Program + { + public static void Main() + { + scoped MyRefStruct + } + } +} +" + state.SendTypeChars("MyR") + state.SendSelectCompletionItem("MyRefStruct") + state.SendTypeChars(" ") + Assert.Equal(expectedText, state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(expectedTextBeforeCaret:=" scoped MyRefStruct ", expectedTextAfterCaret:="") + End Using + End Function + + + Public Async Function TestTypeImportCompletionAfterScopedInTopLevel(showCompletionInArgumentLists As Boolean) As Task + Using state = TestStateFactory.CreateCSharpTestState( + +scoped $$ + +namespace MyNamespace +{ + public ref struct MyRefStruct { } +} +, + showCompletionInArgumentLists:=showCompletionInArgumentLists) + + state.Workspace.GlobalOptions.SetGlobalOption(New OptionKey(CompletionOptionsStorage.ForceExpandedCompletionIndexCreation), True) + state.Workspace.GlobalOptions.SetGlobalOption( + New OptionKey(CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp), True) + + state.SendInvokeCompletionList() + Await state.WaitForAsynchronousOperationsAsync() + Await state.WaitForUIRenderedAsync() + + ' Make sure expander is selected + Await state.SetCompletionItemExpanderStateAndWaitForUiRenderAsync(isSelected:=True) + + Dim expectedText = " +using MyNamespace; + +scoped MyRefStruct + +namespace MyNamespace +{ + public ref struct MyRefStruct { } +} +" + state.SendTypeChars("MyR") + state.SendSelectCompletionItem("MyRefStruct") + state.SendTypeChars(" ") + Assert.Equal(expectedText, state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(expectedTextBeforeCaret:="scoped MyRefStruct ", expectedTextAfterCaret:="") + End Using + End Function + Public Async Function TestCompleteParenthesisForMethodUnderNameofContext(showCompletionInArgumentLists As Boolean) As Task Using state = TestStateFactory.CreateCSharpTestState( diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GlobalKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GlobalKeywordRecommender.cs index 2f87981cf6899..430822733b537 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GlobalKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/GlobalKeywordRecommender.cs @@ -32,13 +32,9 @@ protected override bool IsValidContext(int position, CSharpSyntaxContext context } return - context.IsStatementContext || - context.IsGlobalStatementContext || + context.IsTypeContext || UsingKeywordRecommender.IsUsingDirectiveContext(context, forGlobalKeyword: true, cancellationToken) || context.IsAnyExpressionContext || - context.IsObjectCreationTypeContext || - context.IsIsOrAsTypeContext || - context.IsFunctionPointerTypeArgumentContext || syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) || syntaxTree.IsAfterKeyword(position, SyntaxKind.RefKeyword, cancellationToken) || syntaxTree.IsAfterKeyword(position, SyntaxKind.ReadOnlyKeyword, cancellationToken) || diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InKeywordRecommender.cs index 0f2353cdc2ea9..99c38072b920c 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/InKeywordRecommender.cs @@ -36,7 +36,7 @@ private static bool IsInParameterModifierContext(int position, CSharpSyntaxConte if (context.SyntaxTree.IsParameterModifierContext( position, context.LeftToken, includeOperators: true, out var parameterIndex, out var previousModifier)) { - if (previousModifier == SyntaxKind.None) + if (previousModifier is SyntaxKind.None or SyntaxKind.ScopedKeyword) { return true; } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OutKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OutKeywordRecommender.cs index e04a64f83af4a..3dc302e095c0a 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OutKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/OutKeywordRecommender.cs @@ -31,7 +31,7 @@ private static bool IsOutParameterModifierContext(int position, CSharpSyntaxCont { return context.SyntaxTree.IsParameterModifierContext( position, context.LeftToken, includeOperators: false, out _, out var previousModifier) && - previousModifier == SyntaxKind.None; + previousModifier is SyntaxKind.None or SyntaxKind.ScopedKeyword; } } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RefKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RefKeywordRecommender.cs index 8b369352c738d..b54dff97bf265 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RefKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/RefKeywordRecommender.cs @@ -93,7 +93,7 @@ private static bool IsRefParameterModifierContext(int position, CSharpSyntaxCont if (context.SyntaxTree.IsParameterModifierContext( position, context.LeftToken, includeOperators: false, out var parameterIndex, out var previousModifier)) { - if (previousModifier == SyntaxKind.None) + if (previousModifier is SyntaxKind.None or SyntaxKind.ScopedKeyword) { return true; } @@ -150,6 +150,11 @@ private static bool IsValidRefExpressionContext(CSharpSyntaxContext context) case SyntaxKind.ReturnKeyword: return true; + // scoped ref ... + case SyntaxKind.ScopedKeyword: + case SyntaxKind.IdentifierToken when token.Text == "scoped": + return true; + // { // () => ref ... // diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ScopedKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ScopedKeywordRecommender.cs new file mode 100644 index 0000000000000..ded0f3e44fcb0 --- /dev/null +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ScopedKeywordRecommender.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Utilities; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +{ + internal class ScopedKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + { + public ScopedKeywordRecommender() + : base(SyntaxKind.ScopedKeyword) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var syntaxTree = context.SyntaxTree; + return + syntaxTree.IsParameterModifierContext(position, context.LeftToken, includeOperators: true, out _, out _) || + syntaxTree.IsAnonymousMethodParameterModifierContext(position, context.LeftToken) || + syntaxTree.IsPossibleLambdaParameterModifierContext(position, context.LeftToken, cancellationToken) || + IsValidScopedLocalContext(context); + } + + private static bool IsValidScopedLocalContext(CSharpSyntaxContext context) + { + // scoped ref var x ... + if (context.IsStatementContext || context.IsGlobalStatementContext) + { + return true; + } + + var token = context.TargetToken; + switch (token.Kind()) + { + // for (scoped ref var x ... + // foreach (scoped ... + case SyntaxKind.OpenParenToken: + var previous = token.GetPreviousToken(includeSkipped: true); + return previous.Kind() is SyntaxKind.ForKeyword or SyntaxKind.ForEachKeyword; + + // M(out scoped ..) + case SyntaxKind.OutKeyword: + return token.Parent is ArgumentSyntax; + } + + return false; + } + } +} diff --git a/src/Workspaces/CSharp/Portable/Classification/SyntaxClassification/NameSyntaxClassifier.cs b/src/Workspaces/CSharp/Portable/Classification/SyntaxClassification/NameSyntaxClassifier.cs index b918f11ed2b5d..1f5269be22c56 100644 --- a/src/Workspaces/CSharp/Portable/Classification/SyntaxClassification/NameSyntaxClassifier.cs +++ b/src/Workspaces/CSharp/Portable/Classification/SyntaxClassification/NameSyntaxClassifier.cs @@ -302,6 +302,7 @@ private static bool IsInVarContext(NameSyntax name) { return name.CheckParent(v => v.Type == name) || + name.CheckParent(v => v.Type == name) || name.CheckParent(f => f.Type == name) || name.CheckParent(v => v.Type == name) || name.CheckParent(v => v.Type == name) || diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs index 8c156ed97e27d..fa55c9f470955 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs @@ -334,10 +334,17 @@ public static bool IsWrittenTo( // most cases of `ref x` will count as a potential write of `x`. An important exception is: // `ref readonly y = ref x`. In that case, because 'y' can't be written to, this would not // be a write of 'x'. - if (refParent.Parent is EqualsValueClauseSyntax { Parent: VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Type: RefTypeSyntax refType } } } - && refType.ReadOnlyKeyword != default) + if (refParent.Parent is EqualsValueClauseSyntax { Parent: VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Type: { } variableDeclarationType } } }) { - return false; + if (variableDeclarationType is ScopedTypeSyntax scopedType) + { + variableDeclarationType = scopedType.Type; + } + + if (variableDeclarationType is RefTypeSyntax refType && refType.ReadOnlyKeyword != default) + { + return false; + } } return true; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/SyntaxTreeExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/SyntaxTreeExtensions.cs index df763c1f698be..5ce100d79990e 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/SyntaxTreeExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/SyntaxTreeExtensions.cs @@ -1083,13 +1083,22 @@ parameter2.Parent is ParameterListSyntax parameterList2 && return true; } - if (token.Kind() is SyntaxKind.RefKeyword or SyntaxKind.InKeyword or SyntaxKind.OutKeyword or SyntaxKind.ThisKeyword or SyntaxKind.ParamsKeyword && - token.Parent is ParameterSyntax parameter3 && - parameter3.Parent is ParameterListSyntax parameterList3 && + ParameterSyntax? parameter3 = null; + if (token.Kind() is SyntaxKind.RefKeyword or SyntaxKind.InKeyword or SyntaxKind.OutKeyword or SyntaxKind.ThisKeyword or SyntaxKind.ParamsKeyword or SyntaxKind.ScopedKeyword) + { + parameter3 = token.Parent as ParameterSyntax; + previousModifier = token.Kind(); + } + else if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "scoped" && token.Parent is IdentifierNameSyntax scopedIdentifierName) + { + parameter3 = scopedIdentifierName.Parent as ParameterSyntax; + previousModifier = SyntaxKind.ScopedKeyword; + } + + if (parameter3 is { Parent: ParameterListSyntax parameterList3 } && parameterList3.IsDelegateOrConstructorOrLocalFunctionOrMethodOrOperatorParameterList(includeOperators)) { parameterIndex = parameterList3.Parameters.IndexOf(parameter3); - previousModifier = token.Kind(); return true; } @@ -1230,14 +1239,25 @@ public static bool IsAnonymousMethodParameterModifierContext( var token = tokenOnLeftOfPosition; token = token.GetPreviousTokenIfTouchingWord(position); - if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken && - token.Parent.IsKind(SyntaxKind.ParameterList) && - token.Parent.IsParentKind(SyntaxKind.AnonymousMethodExpression)) + SyntaxNode? parent; + if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken) { - return true; + parent = token.Parent; + } + else if (token.IsKind(SyntaxKind.ScopedKeyword) && token.Parent.IsKind(SyntaxKind.Parameter)) + { + parent = token.Parent.Parent; + } + else if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "scoped" && token.Parent is IdentifierNameSyntax scopedIdentifierName && scopedIdentifierName.Parent.IsKind(SyntaxKind.Parameter)) + { + parent = scopedIdentifierName.Parent.Parent; + } + else + { + return false; } - return false; + return parent.IsKind(SyntaxKind.ParameterList) && parent.IsParentKind(SyntaxKind.AnonymousMethodExpression); } public static bool IsPossibleLambdaOrAnonymousMethodParameterTypeContext( @@ -1762,6 +1782,7 @@ public static bool IsLocalVariableDeclarationContext( // join var // using var // await using var + // scoped var var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position); @@ -1844,6 +1865,23 @@ token.Parent is ArgumentSyntax argument && return true; } + // scoped | + // The compiler parses this as an identifier whose parent is: + // - ExpressionStatementSyntax when in method declaration. + // - IncompleteMemberSyntax when in top-level code and there are no class declarations after it. + // - BaseTypeDeclarationSyntax if it comes after scoped + // - VariableDeclarationSyntax for `scoped X` inside method declaration + if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "scoped" && token.Parent.IsKind(SyntaxKind.IdentifierName) && token.Parent.Parent is VariableDeclarationSyntax or ExpressionStatementSyntax or IncompleteMemberSyntax) + { + return true; + } + + // scoped v| + if (token.IsKind(SyntaxKind.ScopedKeyword) && token.Parent is IncompleteMemberSyntax) + { + return true; + } + return false; }