diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AbstractCSharpCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AbstractCSharpCompletionProviderTests.cs index 1ef020bd47cd8..e6a96469724fe 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AbstractCSharpCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AbstractCSharpCompletionProviderTests.cs @@ -32,7 +32,9 @@ protected static string GetMarkup(string source, LanguageVersion languageVersion => $@" + "; diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AwaitCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AwaitCompletionProviderTests.cs index 2f72a34b5aefa..9a807f476f1e9 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AwaitCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AwaitCompletionProviderTests.cs @@ -24,19 +24,40 @@ public class AwaitCompletionProviderTests : AbstractCSharpCompletionProviderTest { internal override Type GetCompletionProviderType() => typeof(AwaitCompletionProvider); + private const string CompletionDisplayTextAwait = "await"; + private const string CompletionDisplayTextAwaitAndConfigureAwait = "awaitf"; + private async Task VerifyAbsenceAsync(string code) { - await VerifyItemIsAbsentAsync(code, "await"); + await VerifyItemIsAbsentAsync(code, CompletionDisplayTextAwait); + await VerifyItemIsAbsentAsync(code, CompletionDisplayTextAwaitAndConfigureAwait); } - private async Task VerifyAbsenceAsync(string code, LanguageVersion languageVersion) + private async Task VerifyAbsenceAsync(string code, LanguageVersion languageVersion = LanguageVersion.Default) { - await VerifyItemIsAbsentAsync(GetMarkup(code, languageVersion), "await"); + await VerifyItemIsAbsentAsync(GetMarkup(code, languageVersion), CompletionDisplayTextAwait); + await VerifyItemIsAbsentAsync(GetMarkup(code, languageVersion), CompletionDisplayTextAwaitAndConfigureAwait); } - private async Task VerifyKeywordAsync(string code, LanguageVersion languageVersion, string? inlineDescription = null) + private async Task VerifyKeywordAsync(string code, LanguageVersion languageVersion = LanguageVersion.Default, string? inlineDescription = null, bool dotAwait = false, bool dotAwaitf = false) { - await VerifyItemExistsAsync(GetMarkup(code, languageVersion), "await", glyph: (int)Glyph.Keyword, inlineDescription: inlineDescription); + var expectedDescription = dotAwait + ? GetDescription(CompletionDisplayTextAwait, FeaturesResources.Await_the_preceding_expression) + : GetDescription(CompletionDisplayTextAwait, FeaturesResources.Asynchronously_waits_for_the_task_to_finish); + await VerifyItemExistsAsync(GetMarkup(code, languageVersion), CompletionDisplayTextAwait, glyph: (int)Glyph.Keyword, expectedDescriptionOrNull: expectedDescription, inlineDescription: inlineDescription); + + if (dotAwaitf) + { + expectedDescription = string.Format(FeaturesResources.Await_the_preceding_expression_and_add_ConfigureAwait_0, "false"); + await VerifyItemExistsAsync(GetMarkup(code, languageVersion), CompletionDisplayTextAwaitAndConfigureAwait, glyph: (int)Glyph.Keyword, expectedDescriptionOrNull: expectedDescription, inlineDescription: inlineDescription); + } + else + { + await VerifyItemIsAbsentAsync(GetMarkup(code, languageVersion), CompletionDisplayTextAwaitAndConfigureAwait); + } + + static string GetDescription(string keyword, string tooltip) + => $"{string.Format(FeaturesResources._0_Keyword, keyword)}\r\n{tooltip}"; } [Fact] @@ -58,7 +79,7 @@ class C void F() { $$ } -}", LanguageVersion.CSharp9, FeaturesResources.Make_containing_scope_async); +}", LanguageVersion.CSharp9); } [Fact] @@ -102,7 +123,7 @@ Task F() { var z = $$ } } -", LanguageVersion.CSharp9, FeaturesResources.Make_containing_scope_async); +", LanguageVersion.CSharp9); } [Fact] @@ -279,5 +300,602 @@ public async Task TestAwaitInLock_TopLevel() { await VerifyKeywordAsync("lock($$", LanguageVersion.CSharp9); } + + [Fact] + public async Task TestDotAwaitSuggestAfterDotOnTask() + { + await VerifyKeywordAsync(@" +using System.Threading.Tasks; + +class C +{ + async Task F(Task someTask) + { + someTask.$$ + } +} +", dotAwait: true, dotAwaitf: true); + } + + [Fact] + public async Task TestDotAwaitSuggestAfterDotOnTaskOfT() + { + await VerifyKeywordAsync(@" +using System.Threading.Tasks; + +class C +{ + async Task F(Task someTask) + { + someTask.$$ + } +} +", dotAwait: true, dotAwaitf: true); + } + + [Fact] + public async Task TestDotAwaitSuggestAfterDotOnValueTask() + { + var valueTaskAssembly = typeof(ValueTask).Assembly.Location; + var markup = @$" + + + {valueTaskAssembly} + +using System.Threading.Tasks; + +class C +{{ + async Task F(ValueTask someTask) + {{ + someTask.$$ + }} +}} + + + +"; + await VerifyItemExistsAsync(markup, "await"); + await VerifyItemExistsAsync(markup, "awaitf"); + } + + [Fact] + public async Task TestDotAwaitSuggestAfterDotOnCustomAwaitable() + { + await VerifyKeywordAsync(@" +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public class DummyAwaiter: INotifyCompletion { + public bool IsCompleted => true; + public void OnCompleted(Action continuation) => continuation(); + public void GetResult() {} +} + +public class CustomAwaitable +{ + public DummyAwaiter GetAwaiter() => new DummyAwaiter(); +} + +static class Program +{ + static async Task Main() + { + var awaitable = new CustomAwaitable(); + awaitable.$$; + } +}", dotAwait: true); + } + + [Fact] + public async Task TestDotAwaitSuggestAfterDotOnCustomAwaitableButNotConfigureAwaitEvenIfPresent() + { + await VerifyKeywordAsync(@" +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public class DummyAwaiter: INotifyCompletion { + public bool IsCompleted => true; + public void OnCompleted(Action continuation) => continuation(); + public void GetResult() {} +} + +public class CustomAwaitable +{ + public DummyAwaiter GetAwaiter() => new DummyAwaiter(); + public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) => default; +} + +static class Program +{ + static async Task Main() + { + var awaitable = new CustomAwaitable(); + awaitable.$$; + } +}", dotAwait: true, dotAwaitf: false); + } + + [Fact] + public async Task TestDotAwaitSuggestAfterDotDot() + { + await VerifyKeywordAsync(@" +using System.Threading.Tasks; + +static class Program +{ + static async Task Main(Task someTask) + { + someTask.$$.; + } +}", dotAwait: true, dotAwaitf: true); + } + + [Fact] + public async Task TestDotAwaitSuggestAfterDotBeforeType() + { + await VerifyKeywordAsync(@" +using System; +using System.Threading.Tasks; + +static class Program +{ + static async Task Main(Task someTask) + { + someTask.$$ + Int32 i = 0; + } +}", dotAwait: true, dotAwaitf: true); + } + + [Fact] + public async Task TestDotAwaitSuggestAfterDotBeforeAnotherAwait() + { + await VerifyKeywordAsync(@" +using System; +using System.Threading.Tasks; + +static class Program +{ + static async Task Main(Task someTask) + { + someTask.$$ + await Test(); + } + + async Task Test() { } +}", dotAwait: true, dotAwaitf: true); + } + + [Theory] + [InlineData("")] + [InlineData("X.Y Test();")] + [InlineData("var x;")] + [InlineData("int x;")] + [InlineData("System.Int32 x;")] + [InlineData("if (true) { }")] + [InlineData("System.Int32 Test() => 0;")] + [InlineData("async Task Test() => await Task.FromResult(1);")] + public async Task TestDotAwaitSuggestAfterDotBeforeDifferentStatements(string statement) + { + await VerifyKeywordAsync($@" +using System; +using System.Threading.Tasks; + +static class Program +{{ + static async Task Main(Task someTask) + {{ + someTask.$$ + {statement} + }} +}}", dotAwait: true, dotAwaitf: true); + } + + [Theory] + // static + [InlineData("StaticField.$$")] + [InlineData("StaticProperty.$$")] + [InlineData("StaticMethod().$$")] + + // parameters, locals and local function + [InlineData("local.$$")] + [InlineData("parameter.$$")] + [InlineData("LocalFunction().$$")] + + // members + [InlineData("c.Field.$$")] + [InlineData("c.Property.$$")] + [InlineData("c.Method().$$")] + [InlineData("c.Self.Field.$$")] + [InlineData("c.Self.Property.$$")] + [InlineData("c.Self.Method().$$")] + [InlineData("c.Function()().$$")] + + // indexer, operator, conversion + [InlineData("c[0].$$")] + [InlineData("(c + c).$$")] + [InlineData("((Task)c).$$")] + [InlineData("(c as Task).$$")] + + // parenthesized + [InlineData("(parameter).$$")] + [InlineData("((parameter)).$$")] + [InlineData("(true ? parameter : parameter).$$")] + [InlineData("(null ?? Task.CompletedTask).$$")] + public async Task TestDotAwaitSuggestAfterDifferentExpressions(string expression) + { + await VerifyKeywordAsync($@" +using System; +using System.Threading.Tasks; + +class C +{{ + public C Self => this; + public Task Field = Task.CompletedTask; + public Task Method() => Task.CompletedTask; + public Task Property => Task.CompletedTask; + public Task this[int i] => Task.CompletedTask; + public Func Function() => () => Task.CompletedTask; + public static Task operator +(C left, C right) => Task.CompletedTask; + public static explicit operator Task(C c) => Task.CompletedTask; +}} + +static class Program +{{ + static Task StaticField = Task.CompletedTask; + static Task StaticProperty => Task.CompletedTask; + static Task StaticMethod() => Task.CompletedTask; + + static async Task Main(Task parameter) + {{ + Task local = Task.CompletedTask; + var c = new C(); + + {expression} + + Task LocalFunction() => Task.CompletedTask; + }} +}}", dotAwait: true, dotAwaitf: true); + } + + [WorkItem(56245, "https://github.com/dotnet/roslyn/issues/56245")] + [Fact(Skip = "Fails because speculative binding can't figure out that local is a Task.")] + public async Task TestDotAwaitSuggestBeforeLocalFunction() + { + // Speculative binding a local as expression finds the local as ILocalSymbol, but the type is ErrorType. + // This is only the case when + // * await is partially written (local.a), + // * only for locals (e.g. IParameterSymbols are fine) which + // * are declared with var + // * The return type of the local function is used as first name in a MemberAccess in the declarator + await VerifyKeywordAsync(@" +using System.Threading.Tasks; + +static class Program +{ + static async Task Main() + { + var local = Task.CompletedTask; + local.a$$ + + Task LocalFunction() => Task.CompletedTask; + } +}"); + } + + [Theory] + [InlineData("await Task.Run(async () => Task.CompletedTask.$$")] + [InlineData("await Task.Run(async () => someTask.$$")] + [InlineData("await Task.Run(async () => someTask.$$);")] + [InlineData("await Task.Run(async () => { someTask.$$ }")] + [InlineData("await Task.Run(async () => { someTask.$$ });")] + + [InlineData("Task.Run(async () => await someTask).$$")] + + [InlineData("await Task.Run(() => someTask.$$")] + public async Task TestDotAwaitSuggestInLambdas(string lambda) + { + await VerifyKeywordAsync($@" +using System.Threading.Tasks; + +static class Program +{{ + static async Task Main() + {{ + var someTask = Task.CompletedTask; + {lambda} + }} +}}", dotAwait: true, dotAwaitf: true); + } + + [Fact] + public async Task TestDotAwaitNotAfterDotOnTaskIfAlreadyAwaited() + { + await VerifyAbsenceAsync(@" +using System.Threading.Tasks; + +class C +{ + async Task F(Task someTask) + { + await someTask.$$ + } +} +"); + } + + [Fact] + public async Task TestDotAwaitNotAfterTaskType() + { + await VerifyAbsenceAsync(@" +using System.Threading.Tasks; + +class C +{ + async Task F() + { + Task.$$ + } +} +"); + } + + [Fact] + public async Task TestDotAwaitNotInLock() + { + await VerifyAbsenceAsync(@" +using System.Threading.Tasks; + +class C +{ + async Task F(Task someTask) + { + lock(this) { someTask.$$ } + } +} +"); + } + + [Fact] + public async Task TestDotAwaitNotInLock_TopLevel() + { + await VerifyAbsenceAsync(@" +using System.Threading.Tasks; + +lock(this) { Task.CompletedTask.$$ } +"); + } + + [Fact] + public async Task TestDotAwaitQueryNotInSelect() + { + await VerifyAbsenceAsync(@" +using System.Linq; +using System.Threading.Tasks; + +class C +{ + async Task F() + { + var z = from t in new[] { Task.CompletedTask } + select t.$$ + } +} +"); + } + + [Fact] + public async Task TestDotAwaitQueryInFirstFromClause() + { + await VerifyKeywordAsync(@" +using System.Linq; +using System.Threading.Tasks; + +class C +{ + async Task F() + { + var arrayTask2 = Task.FromResult(new int[0]); + var z = from t in arrayTask2.$$ + select t; + } +} +", dotAwait: true, dotAwaitf: true); + } + + [Fact] + public async Task TestDotAwaitQueryNotInSecondFromClause() + { + await VerifyNoItemsExistAsync(@" +using System.Linq; +using System.Threading.Tasks; + +class C +{ + async Task F() + { + var array1 = new int[0]; + var arrayTask2 = Task.FromResult(new int[0]); + var z = from i1 in array1 + from i2 in arrayTask2.$$ + select i2; + } +} +"); + } + + [Fact] + public async Task TestDotAwaitQueryNotInContinuation() + { + await VerifyNoItemsExistAsync(@" +using System.Linq; +using System.Threading.Tasks; + +class C +{ + async Task F() + { + var array1 = new int[0]; + var arrayTask2 = Task.FromResult(new int[0]); + var z = from i1 in array1 + select i1 into c + from i2 in arrayTask2.$$ + select i2; + } +} +"); + } + + [Fact] + public async Task TestDotAwaitQueryInJoinClause() + { + await VerifyKeywordAsync(@" +using System.Linq; +using System.Threading.Tasks; + +class C +{ + async Task F() + { + var array1 = new int[0]; + var arrayTask2 = Task.FromResult(new int[0]); + var z = from i1 in array1 + join i2 in arrayTask2.$$ on i1 equals i2 + select i1; + } +} +", dotAwait: true, dotAwaitf: true); + } + + [Fact] + public async Task TestDotAwaitQueryInJoinIntoClause() + { + await VerifyKeywordAsync(@" +using System.Linq; +using System.Threading.Tasks; + +class C +{ + async Task F() + { + var array1 = new int[0]; + var arrayTask2 = Task.FromResult(new int[0]); + var z = from i1 in array1 + join i2 in arrayTask2.$$ on i1 equals i2 into g + select g; + } +} +", dotAwait: true, dotAwaitf: true); + } + + [Fact] + public async Task TestDotAwaitNotAfterConditionalAccessOfTaskMembers() + { + // The conditional access suggests, that someTask can be null. + // await on null throws at runtime, so the user should do + // if (someTask is not null) await someTask; + // or + // await (someTask ?? Task.CompletedTask) + // Completion should not offer await, because the patterns above would change to much code. + // This decision should be revised after https://github.com/dotnet/csharplang/issues/35 + // is implemented. + await VerifyAbsenceAsync(@" +using System.Threading.Tasks; + +class C +{ + async Task F(Task someTask) + { + someTask?.$$ + } +} +"); + } + + [Theory] + [InlineData("c?.SomeTask.$$")] + + [InlineData("c.M()?.SomeTask.$$")] + [InlineData("c.Pro?.SomeTask.$$")] + + [InlineData("c?.M().SomeTask.$$")] + [InlineData("c?.Pro.SomeTask.$$")] + + [InlineData("c?.M()?.SomeTask.$$")] + [InlineData("c?.Pro?.SomeTask.$$")] + + [InlineData("c.M()?.Pro.SomeTask.$$")] + [InlineData("c.Pro?.M().SomeTask.$$")] + + [InlineData("c.M()?.M().M()?.M().SomeTask.$$")] + [InlineData("new C().M()?.Pro.M()?.M().SomeTask.$$")] + public async Task TestDotAwaitNotAfterDotInConditionalAccessChain(string conditionalAccess) + { + await VerifyAbsenceAsync($@" +using System.Threading.Tasks; +public class C +{{ + public Task SomeTask => Task.CompletedTask; + + public C Pro => this; + public C M() => this; +}} + +static class Program +{{ + public static async Task Main() + {{ + var c = new C(); + {conditionalAccess} + }} +}} +"); + } + + [Theory] + [InlineData("c!.SomeTask.$$")] + [InlineData("c.SomeTask!.$$")] + + [InlineData("c.M()!.SomeTask.$$")] + [InlineData("c.Pro!.SomeTask.$$")] + + [InlineData("c!.M().SomeTask.$$")] + [InlineData("c!.Pro.SomeTask.$$")] + + [InlineData("c!.M()!.SomeTask.$$")] + [InlineData("c!.Pro!.SomeTask.$$")] + + [InlineData("c.M()!.Pro.SomeTask.$$")] + [InlineData("c.Pro!.M().SomeTask.$$")] + + [InlineData("c.M()!.M().M()!.M().SomeTask.$$")] + [InlineData("new C().M()!.Pro.M()!.M().SomeTask.$$")] + public async Task TestDotAwaitAfterNullForgivingOperatorAccessChain(string nullForgivingAccess) + { + await VerifyKeywordAsync($@" +#nullable enable + +using System.Threading.Tasks; +public class C +{{ + public Task? SomeTask => Task.CompletedTask; + + public C? Pro => this; + public C? M() => this; +}} + +static class Program +{{ + public static async Task Main(params string[] args) + {{ + var c = args[1] == string.Empty ? new C() : null; + {nullForgivingAccess} + }} +}} +", dotAwait: true, dotAwaitf: true); + } } } diff --git a/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests_AwaitCompletion.vb b/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests_AwaitCompletion.vb index 71ab0e7b76262..1f92cf4fc1764 100644 --- a/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests_AwaitCompletion.vb +++ b/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests_AwaitCompletion.vb @@ -8,6 +8,44 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense Public Class CSharpCompletionCommandHandlerTests_Await + + Private Shared Function GetTestClassDocument(containerHasAsyncModifier As Boolean, testExpression As String) As XElement + Return _ + +using System; +using System.Threading.Tasks; + +class C +{ + public C Self => this; + public Task Field = Task.CompletedTask; + public Task Method() => Task.CompletedTask; + public Task Property => Task.CompletedTask; + public Task this[int i] => Task.CompletedTask; + public Func<Task> Function() => () => Task.CompletedTask; + public static Task operator +(C left, C right) => Task.CompletedTask; + public static explicit operator Task(C c) => Task.CompletedTask; +} + +static class Program +{ + static Task StaticField = Task.CompletedTask; + static Task StaticProperty => Task.CompletedTask; + static Task StaticMethod() => Task.CompletedTask; + + static<%= If(containerHasAsyncModifier, " async", "") %> Task Main(Task parameter) + { + Task local = Task.CompletedTask; + var c = new C(); + + <%= testExpression %> + + Task LocalFunction() => Task.CompletedTask; + } +} + + End Function + Public Async Function AwaitCompletionAddsAsync_MethodDeclaration() As Task Using state = TestStateFactory.CreateCSharpTestState( @@ -24,7 +62,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -60,7 +98,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -96,7 +134,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -130,7 +168,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -164,7 +202,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -198,7 +236,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -232,7 +270,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -266,7 +304,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -301,7 +339,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -335,7 +373,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -368,7 +406,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -386,6 +424,431 @@ public class C End Function + Public Async Function DotAwaitCompletionAddsAwaitInFrontOfExpression() As Task + Using state = TestStateFactory.CreateCSharpTestState( + + ) + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +using System.Threading.Tasks; + +public class C +{ + public static async Task Main() + { + await Task.CompletedTask + } +} +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" await Task.CompletedTask", "") + End Using + End Function + + + Public Async Function DotAwaitCompletionAddsAwaitInFrontOfExpressionAndAsyncModifier() As Task + Using state = TestStateFactory.CreateCSharpTestState( + + ) + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +using System.Threading.Tasks; + +public class C +{ + public static async Task Main() + { + await Task.CompletedTask + } +} +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" await Task.CompletedTask", "") + End Using + End Function + + + Public Async Function DotAwaitCompletionAddsAwaitInFrontOfExpressionAndAppendsConfigureAwait() As Task + Using state = TestStateFactory.CreateCSharpTestState( + + ) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContainAll("await", "awaitf") + state.SendTypeChars("a") + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) + state.SendTypeChars("f") + Await state.AssertSelectedCompletionItem(displayText:="awaitf", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +using System.Threading.Tasks; + +public class C +{ + public static async Task Main() + { + await Task.CompletedTask.ConfigureAwait(false) + } +} +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" await Task.CompletedTask.ConfigureAwait(false)", "") + End Using + End Function + + + + + + + + + + + + + + + + + + + + + + + + + Public Async Function DotAwaitCompletionAddsAwaitInFrontOfExpressionForDifferentExpressions(expression As String, committed As String) As Task + ' place await in front of expression + Using state = TestStateFactory.CreateCSharpTestState(GetTestClassDocument(containerHasAsyncModifier:=True, expression)) + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) + + state.SendTab() + Assert.Equal(GetTestClassDocument(containerHasAsyncModifier:=True, committed).Value.NormalizeLineEndings(), state.GetDocumentText().NormalizeLineEndings()) + Await state.AssertLineTextAroundCaret($" {committed}", "") + End Using + + ' place await in front of expression and make container async + Using state = TestStateFactory.CreateCSharpTestState(GetTestClassDocument(containerHasAsyncModifier:=False, expression)) + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) + + state.SendTab() + Assert.Equal(GetTestClassDocument(containerHasAsyncModifier:=True, committed).Value.NormalizeLineEndings(), state.GetDocumentText().NormalizeLineEndings()) + Await state.AssertLineTextAroundCaret($" {committed}", "") + End Using + + ' ConfigureAwait(false) starts here + committed += ".ConfigureAwait(false)" + ' place await in front of expression and append ConfigureAwait(false) + Using state = TestStateFactory.CreateCSharpTestState(GetTestClassDocument(containerHasAsyncModifier:=True, expression)) + state.SendTypeChars("af") + Await state.AssertSelectedCompletionItem(displayText:="awaitf", isHardSelected:=True) + + state.SendTab() + Assert.Equal(GetTestClassDocument(containerHasAsyncModifier:=True, committed).Value.NormalizeLineEndings(), state.GetDocumentText().NormalizeLineEndings()) + Await state.AssertLineTextAroundCaret($" {committed}", "") + End Using + + ' place await in front of expression, append ConfigureAwait(false) and make container async + Using state = TestStateFactory.CreateCSharpTestState(GetTestClassDocument(containerHasAsyncModifier:=False, expression)) + state.SendTypeChars("af") + Await state.AssertSelectedCompletionItem(displayText:="awaitf", isHardSelected:=True) + + state.SendTab() + Assert.Equal(GetTestClassDocument(containerHasAsyncModifier:=True, committed).Value.NormalizeLineEndings(), state.GetDocumentText().NormalizeLineEndings()) + Await state.AssertLineTextAroundCaret($" {committed}", "") + End Using + End Function + + + Task.CompletedTask.$$", + "await Task.Run(async () => await Task.CompletedTask$$")> + Task.CompletedTask.$$", + "await Task.Run(async () => await Task.CompletedTask$$")> + Task.CompletedTask.aw$$", + "await Task.Run(async () => await Task.CompletedTask$$")> + Task.CompletedTask.aw$$", + "await Task.Run(async () => await Task.CompletedTask$$")> + someTask.$$", + "await Task.Run(async () => await someTask$$")> + someTask.$$", + "await Task.Run(async () => await someTask$$")> + someTask.$$);", + "await Task.Run(async () => await someTask$$);")> + someTask.$$);", + "await Task.Run(async () => await someTask$$);")> + someTask.aw$$);", + "await Task.Run(async () => await someTask$$);")> + someTask.aw$$);", + "await Task.Run(async () => await someTask$$);")> + {someTask.$$}", + "await Task.Run(async () => {await someTask$$}")> + {someTask.$$}", + "await Task.Run(async () => {await someTask$$}")> + {someTask.$$});", + "await Task.Run(async () => {await someTask$$});")> + {someTask.$$});", + "await Task.Run(async () => {await someTask$$});")> + someTask. $$ );", + "await Task.Run(async () => await someTask$$ );")> + someTask. $$ );", + "await Task.Run(async () => await someTask$$ );")> + someTask . $$ );", + "await Task.Run(async () => await someTask $$ );")> + someTask . $$ );", + "await Task.Run(async () => await someTask $$ );")> + someTask.$$.);", + "await Task.Run(async () => await someTask$$.);")> + someTask.$$.);", + "await Task.Run(async () => await someTask$$.);")> + await someTask).$$", + "await Task.Run(async () => await someTask)$$")> + Public Async Function DotAwaitCompletionAddsAwaitInFrontOfExpressionInLambdas(expression As String, committed As String) As Task + + Dim document As XElement = +using System.Threading.Tasks; + +static class Program +{ + static async Task Main() + { + var someTask = Task.CompletedTask; + <%= expression %> + } +} + + ' Test await completion + Using state = TestStateFactory.CreateCSharpTestState(document) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContainAll("await", "awaitf") + state.SendTypeChars("a") + state.SendTypeChars("w") + Await state.AssertSelectedCompletionItem("await") + + state.SendTab() + Dim committedAwait = committed + Dim committedCursorPosition = committedAwait.IndexOf("$$") + committedAwait = committedAwait.Replace("$$", "") + Assert.Equal($" +using System.Threading.Tasks; + +static class Program +{{ + static async Task Main() + {{ + var someTask = Task.CompletedTask; + {committedAwait} + }} +}} +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret($" {committedAwait.Substring(0, committedCursorPosition)}", committedAwait.Substring(committedCursorPosition)) + End Using + + ' Test awaitf completion + Using state = TestStateFactory.CreateCSharpTestState(document) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContainAll("await", "awaitf") + state.SendTypeChars("a") + state.SendTypeChars("f") + Await state.AssertSelectedCompletionItem("awaitf") + + state.SendTab() + Dim committedAwaitf = committed + Dim committedCursorPosition = committedAwaitf.IndexOf("$$") + committedAwaitf = committedAwaitf.Replace("$$", "") + Dim committedAwaitfBeforeCursor = committedAwaitf.Substring(0, committedCursorPosition) + Dim committedAwaitfAfterCursor = committedAwaitf.Substring(committedCursorPosition) + Assert.Equal($" +using System.Threading.Tasks; + +static class Program +{{ + static async Task Main() + {{ + var someTask = Task.CompletedTask; + {committedAwaitfBeforeCursor}.ConfigureAwait(false){committedAwaitfAfterCursor} + }} +}} +", state.GetDocumentText()) + ' the expected cursor position is right after the inserted .ConfigureAwait(false) + Await state.AssertLineTextAroundCaret($" {committedAwaitfBeforeCursor}.ConfigureAwait(false)", committedAwaitfAfterCursor) + End Using + End Function + + + Public Async Function DotAwaitCompletionOffersAwaitAfterConfigureAwaitInvocation() As Task + Using state = TestStateFactory.CreateCSharpTestState( + + ) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContainAll("await") + Await state.AssertCompletionItemsDoNotContainAny("awaitf") + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +using System.Threading.Tasks; + +public class C +{ + public static async Task Main() + { + await Task.CompletedTask.ConfigureAwait(false) + } +} +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" await Task.CompletedTask.ConfigureAwait(false)", "") + End Using + End Function + + + Public Async Function DotAwaitCompletionOffersAwaitBeforeConfigureAwaitInvocation() As Task + Using state = TestStateFactory.CreateCSharpTestState( + + ) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContainAll("await", "awaitf") + state.SendTypeChars("af") + Await state.AssertSelectedCompletionItem(displayText:="awaitf", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +using System.Threading.Tasks; + +public class C +{ + public static async Task Main() + { + await Task.CompletedTask.ConfigureAwait(false)ConfigureAwait(false); + } +} +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" await Task.CompletedTask.ConfigureAwait(false)", "ConfigureAwait(false);") + End Using + End Function + Public Async Function SyntaxIsLikeLocalFunction() As Task Using state = TestStateFactory.CreateCSharpTestState( @@ -402,7 +865,7 @@ public class C ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) state.SendTab() @@ -419,5 +882,144 @@ public class C ", state.GetDocumentText()) End Using End Function + + Public Async Function DotAwaitCompletionInQueryInFirstFromClause() As Task + Using state = TestStateFactory.CreateCSharpTestState( + + ) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContainAll("await", "awaitf") + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="await", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +using System.Linq; +using System.Threading.Tasks; + +class C +{ + async Task F() + { + var arrayTask2 = Task.FromResult(new int[0]); + var z = from i1 in await arrayTask2 + select i1; + } +} +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" var z = from i1 in await arrayTask2", "") + End Using + End Function + + + Public Async Function DotAwaitCompletionInQueryInFirstFromClauseConfigureAwait() As Task + Using state = TestStateFactory.CreateCSharpTestState( + + ) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContainAll("await", "awaitf") + state.SendTypeChars("af") + Await state.AssertSelectedCompletionItem(displayText:="awaitf", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +using System.Linq; +using System.Threading.Tasks; + +class C +{ + async Task F() + { + var arrayTask2 = Task.FromResult(new int[0]); + var z = from i1 in await arrayTask2.ConfigureAwait(false) + select i1; + } +} +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" var z = from i1 in await arrayTask2.ConfigureAwait(false)", "") + End Using + End Function + + + Public Async Function DotAwaitCompletionNullForgivingOperatorIsKept() As Task + Using state = TestStateFactory.CreateCSharpTestState( + Task.CompletedTask; + + public C? Pro => this; + public C? M() => this; +} + +static class Program +{ + public static async Task Main(params string[] args) + { + var c = args[1] == string.Empty ? new C() : null; + c!.SomeTask!.$$; + } +} +]]> + ) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContainAll("await", "awaitf") + state.SendTypeChars("af") + Await state.AssertSelectedCompletionItem(displayText:="awaitf", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +#nullable enable + +using System.Threading.Tasks; +public class C +{ + public Task? SomeTask => Task.CompletedTask; + + public C? Pro => this; + public C? M() => this; +} + +static class Program +{ + public static async Task Main(params string[] args) + { + var c = args[1] == string.Empty ? new C() : null; + await c!.SomeTask!.ConfigureAwait(false); + } +} +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" await c!.SomeTask!.ConfigureAwait(false)", ";") + End Using + End Function End Class End Namespace diff --git a/src/EditorFeatures/Test2/IntelliSense/VisualBasicCompletionCommandHandlerTests_AwaitCompletion.vb b/src/EditorFeatures/Test2/IntelliSense/VisualBasicCompletionCommandHandlerTests_AwaitCompletion.vb index bedc2c7c497e9..93c42335f48d7 100644 --- a/src/EditorFeatures/Test2/IntelliSense/VisualBasicCompletionCommandHandlerTests_AwaitCompletion.vb +++ b/src/EditorFeatures/Test2/IntelliSense/VisualBasicCompletionCommandHandlerTests_AwaitCompletion.vb @@ -8,6 +8,66 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense Public Class VisualBasicCompletionCommandHandlerTests_Await + + Private Shared Function GetTestClassDocument(containerHasAsyncModifier As Boolean, testExpression As String) As XElement + Return _ + +Imports System +Imports System.Threading.Tasks + +Class C + Public ReadOnly Property Self As C + Get + Return Me + End Get + End Property + + Public Field As Task = Task.CompletedTask + + Public Function Method() As Task + Return Task.CompletedTask + End Function + + Public ReadOnly Property [Property] As Task + Get + Return Task.CompletedTask + End Get + End Property + + Default Public ReadOnly Property Item(ByVal i As Integer) As Task + Get + Return Task.CompletedTask + End Get + End Property + + Public Function Func() As Func(Of Task) + Return Function() Task.CompletedTask + End Function +End Class + +Module Program + Shared StaticField As Task = Task.CompletedTask + + Private Shared ReadOnly Property StaticProperty As Task + Get + Return Task.CompletedTask + End Get + End Property + + Private Function StaticMethod() As Task + Return Task.CompletedTask + End Function + + Private<%= If(containerHasAsyncModifier, " Async", "") %> Function Main(ByVal parameter As Task) As Task + Dim local As Task = Task.CompletedTask + Dim c = New C() + + <%= testExpression %> + End Function +End Module + + End Function + Public Async Function AwaitCompletionAddsAsync_FunctionDeclaration() As Task Using state = TestStateFactory.CreateVisualBasicTestState( @@ -22,7 +82,7 @@ End Class ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -49,7 +109,7 @@ End Class ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -78,7 +138,7 @@ End Class ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -111,7 +171,7 @@ End Class ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -142,7 +202,7 @@ End Class ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -171,7 +231,7 @@ End Class ]]> ) state.SendTypeChars("aw") - Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True, inlineDescription:=FeaturesResources.Make_containing_scope_async) + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) state.SendTab() Assert.Equal(" @@ -363,5 +423,276 @@ End Class ", state.GetDocumentText()) End Using End Function + + + Public Async Function DotAwaitCompletionAddsAwaitInFrontOfExpression() As Task + Using state = TestStateFactory.CreateVisualBasicTestState( + + ) + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +Imports System.Threading.Tasks + +Public Class C + Public Shared Async Function Main() As Task + Await Task.CompletedTask + End Function +End Class +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" Await Task.CompletedTask", "") + End Using + End Function + + + Public Async Function DotAwaitCompletionAddsAwaitInFrontOfExpressionAndAsyncModifier() As Task + Using state = TestStateFactory.CreateVisualBasicTestState( + + ) + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +Imports System.Threading.Tasks + +Public Class C + Public Shared Async Function Main() As Task + Await Task.CompletedTask + End Function +End Class +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" Await Task.CompletedTask", "") + End Using + End Function + + + Public Async Function DotAwaitCompletionAddsAwaitInFrontOfExpressionAndAppendsConfigureAwait() As Task + Using state = TestStateFactory.CreateVisualBasicTestState( + + ) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContainAll("Await", "Awaitf") + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) + state.SendTypeChars("f") + Await state.AssertSelectedCompletionItem(displayText:="Awaitf", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +Imports System.Threading.Tasks + +Public Class C + Public Shared Async Function Main() As Task + Await Task.CompletedTask.ConfigureAwait(False) + End Function +End Class +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" Await Task.CompletedTask.ConfigureAwait(False)", "") + End Using + End Function + + + + + + + + + + + + + + + + + + + + + + + Public Async Function DotAwaitCompletionAddsAwaitInFrontOfExpressionForDifferentExpressions(expression As String, committed As String) As Task + ' place await in front of expression + Using state = TestStateFactory.CreateVisualBasicTestState(GetTestClassDocument(containerHasAsyncModifier:=True, expression)) + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) + + state.SendTab() + Assert.Equal(GetTestClassDocument(containerHasAsyncModifier:=True, committed).Value.NormalizeLineEndings(), state.GetDocumentText().NormalizeLineEndings()) + Await state.AssertLineTextAroundCaret($" {committed}", "") + End Using + + ' place await in front of expression and make container async + Using state = TestStateFactory.CreateVisualBasicTestState(GetTestClassDocument(containerHasAsyncModifier:=False, expression)) + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) + + state.SendTab() + Assert.Equal(GetTestClassDocument(containerHasAsyncModifier:=True, committed).Value.NormalizeLineEndings(), state.GetDocumentText().NormalizeLineEndings()) + Await state.AssertLineTextAroundCaret($" {committed}", "") + End Using + + ' ConfigureAwait(false) starts here + committed += ".ConfigureAwait(False)" + ' place await in front of expression and append ConfigureAwait(false) + Using state = TestStateFactory.CreateVisualBasicTestState(GetTestClassDocument(containerHasAsyncModifier:=True, expression)) + state.SendTypeChars("awf") + Await state.AssertSelectedCompletionItem(displayText:="Awaitf", isHardSelected:=True) + + state.SendTab() + Assert.Equal(GetTestClassDocument(containerHasAsyncModifier:=True, committed).Value.NormalizeLineEndings(), state.GetDocumentText().NormalizeLineEndings()) + Await state.AssertLineTextAroundCaret($" {committed}", "") + End Using + + ' place await in front of expression, append ConfigureAwait(false) and make container async + Using state = TestStateFactory.CreateVisualBasicTestState(GetTestClassDocument(containerHasAsyncModifier:=False, expression)) + state.SendTypeChars("awf") + Await state.AssertSelectedCompletionItem(displayText:="Awaitf", isHardSelected:=True) + + state.SendTab() + Assert.Equal(GetTestClassDocument(containerHasAsyncModifier:=True, committed).Value.NormalizeLineEndings(), state.GetDocumentText().NormalizeLineEndings()) + Await state.AssertLineTextAroundCaret($" {committed}", "") + End Using + End Function + + + Public Async Function DotAwaitCompletionInQueryInFirstFromClause() As Task + Using state = TestStateFactory.CreateVisualBasicTestState( + + ) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContainAll("Await", "Awaitf") + state.SendTypeChars("aw") + Await state.AssertSelectedCompletionItem(displayText:="Await", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +Imports System.Linq +Imports System.Threading.Tasks + +Class C + Private Async Function F() As Task + Dim arrayTask1 = Task.FromResult(new Integer() {}) + Dim qry = From i in Await arrayTask1 + End Function +End Class +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" Dim qry = From i in Await arrayTask1", "") + End Using + End Function + + + Public Async Function DotAwaitCompletionInQueryInFirstFromClauseConfigureAwait() As Task + Using state = TestStateFactory.CreateVisualBasicTestState( + + ) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContainAll("Await", "Awaitf") + state.SendTypeChars("af") + Await state.AssertSelectedCompletionItem(displayText:="Awaitf", isHardSelected:=True) + + state.SendTab() + Assert.Equal(" +Imports System.Linq +Imports System.Threading.Tasks + +Class C + Private Async Function F() As Task + Dim arrayTask1 = Task.FromResult(new Integer() {}) + Dim qry = From i in Await arrayTask1.ConfigureAwait(False) + End Function +End Class +", state.GetDocumentText()) + Await state.AssertLineTextAroundCaret(" Dim qry = From i in Await arrayTask1.ConfigureAwait(False)", "") + End Using + End Function End Class End Namespace diff --git a/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/AwaitCompletionProviderTests.vb b/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/AwaitCompletionProviderTests.vb index bcc90954bac5e..7465f4196ca46 100644 --- a/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/AwaitCompletionProviderTests.vb +++ b/src/EditorFeatures/VisualBasicTest/Completion/CompletionProviders/AwaitCompletionProviderTests.vb @@ -12,42 +12,57 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.Completion.Complet Return GetType(AwaitCompletionProvider) End Function + Protected Async Function VerifyAwaitKeyword(markup As String, Optional dotAwait As Boolean = False, Optional dotAwaitf As Boolean = False) As Task + Dim expectedDescription = If(dotAwait, GetDescription("Await", FeaturesResources.Await_the_preceding_expression), GetDescription("Await", FeaturesResources.Asynchronously_waits_for_the_task_to_finish)) + Await VerifyItemExistsAsync(markup, "Await", expectedDescriptionOrNull:=expectedDescription) + If dotAwaitf Then + expectedDescription = String.Format(FeaturesResources.Await_the_preceding_expression_and_add_ConfigureAwait_0, "False") + Await VerifyItemExistsAsync(markup, "Awaitf", expectedDescriptionOrNull:=expectedDescription) + Else + Await VerifyItemIsAbsentAsync(markup, "Awaitf") + End If + End Function + + Private Shared Function GetDescription(ByVal keyword As String, ByVal tooltip As String) As String + Return $"{String.Format(FeaturesResources._0_Keyword, keyword)}{vbCrLf}{tooltip}" + End Function + - Public Sub InSynchronousMethodTest() - VerifyItemExistsAsync(" + Public Async Function InSynchronousMethodTest() As Task + Await VerifyAwaitKeyword(" Class C Sub Goo() Dim z = $$ End Sub End Class -", "Await") - End Sub +") + End Function - Public Sub InMethodStatementTest() - VerifyItemExistsAsync(" + Public Async Function InMethodStatementTest() As Task + Await VerifyAwaitKeyword(" Class C Async Sub Goo() $$ End Sub End Class -", "Await") - End Sub +") + End Function - Public Sub InMethodExpressionTest() - VerifyItemExistsAsync(" + Public Async Function InMethodExpressionTest() As Task + Await VerifyAwaitKeyword(" Class C Async Sub Goo() Dim z = $$ End Sub End Class -", "Await") - End Sub +") + End Function - Public Sub NotInCatchTest() - VerifyItemExistsAsync(" + Public Async Function NotInCatchTest() As Task + Await VerifyNoItemsExistAsync(" Class C Async Sub Goo() Try @@ -57,12 +72,12 @@ Class C End Sub End Class -", "Await") - End Sub +") + End Function - Public Sub NotInCatchExceptionFilterTest() - VerifyNoItemsExistAsync(" + Public Async Function NotInCatchExceptionFilterTest() As Task + Await VerifyNoItemsExistAsync(" Class C Async Sub Goo() Try @@ -72,11 +87,11 @@ Class C End Sub End Class ") - End Sub + End Function - Public Sub InCatchNestedDelegateTest() - VerifyItemExistsAsync(" + Public Async Function InCatchNestedDelegateTest() As Task + Await VerifyAwaitKeyword(" Class C Async Sub Goo() Try @@ -86,12 +101,12 @@ Class C End Sub End Class -", "Await") - End Sub +") + End Function - Public Sub NotInFinallyTest() - VerifyItemExistsAsync(" + Public Async Function NotInFinallyTest() As Task + Await VerifyNoItemsExistAsync(" Class C Async Sub Goo() Try @@ -101,12 +116,12 @@ Class C End Sub End Class -", "Await") - End Sub +") + End Function - Public Sub NotInSyncLockTest() - VerifyItemExistsAsync(" + Public Async Function NotInSyncLockTest() As Task + Await VerifyNoItemsExistAsync(" Class C Async Sub Goo() SyncLock True @@ -114,7 +129,471 @@ Class C End SyncLock End Sub End Class -", "Await") - End Sub +") + End Function + + + Public Async Function DotAwaitInAsyncSub() As Task + Await VerifyAwaitKeyword(" +Imports System.Threading.Tasks + +Class C + Async Sub Goo() + Task.CompletedTask.$$ + End Sub +End Class +", dotAwait:=True, dotAwaitf:=True) + End Function + + + Public Async Function DotAwaitSuggestAfterDotOnTaskOfT() As Task + Await VerifyAwaitKeyword(" +Imports System.Threading.Tasks + +Class C + Private Async Function F(ByVal someTask As Task(Of Integer)) As Task + someTask.$$ + End Function +End Class +", dotAwait:=True, dotAwaitf:=True) + End Function + + + Public Async Function DotAwaitSuggestAfterDotOnValueTask() As Task + Dim valueTaskAssembly = GetType(ValueTask).Assembly.Location + + Await VerifyAwaitKeyword($" + + + {valueTaskAssembly} + +Imports System.Threading.Tasks + +Class C + Private Async Function F(ByVal someTask As ValueTask) As Task + someTask.$$ + End Function +End Class + + + +", dotAwait:=True, dotAwaitf:=True) + End Function + + + Public Async Function DotAwaitSuggestAfterDotOnCustomAwaitable() As Task + Await VerifyAwaitKeyword(" +Imports System +Imports System.Runtime.CompilerServices +Imports System.Threading.Tasks + +Public Class DummyAwaiter + Implements INotifyCompletion + + Public ReadOnly Property IsCompleted As Boolean + Get + Return True + End Get + End Property + + Public Sub OnCompleted(ByVal continuation As Action) + Return continuation() + End Sub + + Public Sub GetResult() + End Sub +End Class + +Public Class CustomAwaitable + Public Function GetAwaiter() As DummyAwaiter + Return New DummyAwaiter() + End Function +End Class + +Module Program + Private Async Function Main() As Task + Dim awaitable = New CustomAwaitable() + awaitable.$$ + End Function +End Module +", dotAwait:=True) + End Function + + + Public Async Function DotAwaitSuggestAfterDotBeforeType() As Task + Await VerifyAwaitKeyword(" +Imports System +Imports System.Threading.Tasks + +Module Program + Private Async Function Main(ByVal someTask As Task) As Task + someTask.$$ + Dim i As Int32 = 0 + End Function +End Module +", dotAwait:=True, dotAwaitf:=True) + End Function + + + Public Async Function DotAwaitSuggestAfterDotBeforeAnotherAwait() As Task + Await VerifyAwaitKeyword(" +Imports System +Imports System.Threading.Tasks + +Module Program + Private Async Function Main(ByVal someTask As Task) As Task + someTask.$$ + Await Test() + End Function + + Private Async Function Test() As Task + End Function +End Module +", dotAwait:=True, dotAwaitf:=True) + End Function + + + + + + + + + + + + + + + + + + + + + + Public Async Function DotAwaitSuggestAfterDifferentExpressions(ByVal expression As String) As Task + Dim t = If(True, expression, expression).Length + Await VerifyAwaitKeyword($" +Imports System +Imports System.Threading.Tasks + +Class C + Public ReadOnly Property Self As C + Get + Return Me + End Get + End Property + + Public Field As Task = Task.CompletedTask + + Public Function Method() As Task + Return Task.CompletedTask + End Function + + Public ReadOnly Property [Property] As Task + Get + Return Task.CompletedTask + End Get + End Property + + Default Public ReadOnly Property Item(ByVal i As Integer) As Task + Get + Return Task.CompletedTask + End Get + End Property + + Public Function Func() As Func(Of Task) + Return Function() Task.CompletedTask + End Function +End Class + +Module Program + Shared StaticField As Task = Task.CompletedTask + + Private Shared ReadOnly Property StaticProperty As Task + Get + Return Task.CompletedTask + End Get + End Property + + Private Function StaticMethod() As Task + Return Task.CompletedTask + End Function + + Private Async Function Main(ByVal parameter As Task) As Task + Dim local As Task = Task.CompletedTask + Dim c = New C() + + {expression} + + End Function +End Module +", dotAwait:=True, dotAwaitf:=True) + End Function + + + + + + + + + Public Async Function DotAwaitSuggestInLambdas(lambda As String) As Task + Await VerifyAwaitKeyword($" +Imports System.Threading.Tasks + +Module Program + Private Async Function Main() As Task + Dim someTask = Task.CompletedTask + + {lambda} + End Function +End Module +", dotAwait:=True, dotAwaitf:=True) + End Function + + + Public Async Function DotAwaitNotAfterDotOnTaskIfAlreadyAwaited() As Task + Await VerifyNoItemsExistAsync(" +Imports System.Threading.Tasks + +Class C + Private Async Function F(ByVal someTask As Task) As Task + Await someTask.$$ + End Function +End Class +") + End Function + + + Public Async Function DotAwaitNotAfterTaskType() As Task + Await VerifyNoItemsExistAsync(" +Imports System.Threading.Tasks + +Class C + Private Async Function F() As Task + Task.$$ + End Function +End Class +") + End Function + + + Public Async Function DotAwaitNotInLock() As Task + Await VerifyNoItemsExistAsync(" +Imports System.Threading.Tasks + +Class C + Private Async Function F(ByVal someTask As Task) As Task + SyncLock Me + someTask.$$ + End SyncLock + End Function +End Class +") + End Function + + + Public Async Function DotAwaitQueryNotInSelect() As Task + Await VerifyNoItemsExistAsync(" +Imports System.Linq +Imports System.Threading.Tasks + +Class C + Private Async Function F() As Task + Dim z = From t In {Task.CompletedTask} Select t.$$ + End Function +End Class +") + End Function + + + Public Async Function DotAwaitQueryInFirstFromClause() As Task + Await VerifyAwaitKeyword(" +Imports System.Linq +Imports System.Threading.Tasks + +Class C + Private Async Function F() As Task + Dim arrayTask1 = Task.FromResult(new Integer() {}) + Dim qry = From i In arrayTask1.$$ + End Function +End Class +", dotAwait:=True, dotAwaitf:=True) + End Function + + + Public Async Function DotAwaitQueryNotInSecondFromClause_1() As Task + Await VerifyNoItemsExistAsync(" +Imports System.Linq +Imports System.Threading.Tasks + +Class C + Private Async Function F() As Task + Dim array1 = new Integer() {} + Dim arrayTask2 = Task.FromResult(new Integer() {}) + + Dim qry = From i1 In array1 + From i2 In arrayTask2.$$ + End Function +End Class +") + End Function + + + Public Async Function DotAwaitQueryNotInSecondFromClause_2() As Task + Await VerifyNoItemsExistAsync(" +Imports System.Linq +Imports System.Threading.Tasks + +Class C + Private Async Function F() As Task + Dim array1 = new Integer() {} + Dim arrayTask2 = Task.FromResult(new Integer() {}) + + Dim qry = From i1 In array1, i2 In arrayTask2.$$ + End Function +End Class +") + End Function + + + Public Async Function DotAwaitQueryNotInSecondFromClause_3() As Task + Await VerifyNoItemsExistAsync(" +Imports System.Linq +Imports System.Threading.Tasks + +Class C + Private Async Function F() As Task + Dim array1 = new Integer() {} + Dim arrayTask2 = Task.FromResult(new Integer() {}) + + Dim qry = From i1 In array1 + From i2 In array1, i2 In arrayTask2.$$ + End Function +End Class +") + End Function + + + Public Async Function DotAwaitQueryNotInSecondFromClause_4() As Task + Await VerifyNoItemsExistAsync(" +Imports System.Linq +Imports System.Threading.Tasks + +Class C + Private Async Function F() As Task + Dim array1 = new Integer() {} + Dim arrayTask2 = Task.FromResult(new Integer() {}) + + Dim qry = From i1 In array1, i2 In array1 + From i3 In arrayTask2.$$ + End Function +End Class +") + End Function + + + Public Async Function DotAwaitQueryInJoinClause() As Task + Await VerifyAwaitKeyword(" +Imports System.Linq +Imports System.Threading.Tasks + +Class C + Private Async Function F() As Task + Dim array1 = new Integer() {} + Dim arrayTask2 = Task.FromResult(new Integer() {}) + + Dim qry = From i1 In array1 + Join i2 In arrayTask2.$$ + On i1 Equals i2 + End Function +End Class +", dotAwait:=True, dotAwaitf:=True) + End Function + + + Public Async Function DotAwaitQueryInGroupJoinClause() As Task + Await VerifyAwaitKeyword(" +Imports System.Linq +Imports System.Threading.Tasks + +Class C + Private Async Function F() As Task + Dim array1 = new Integer() {} + Dim arrayTask2 = Task.FromResult(new Integer() {}) + + Dim qry = From i1 In array1 + Group Join i2 In arrayTask2.$$ + On i1 Equals i2 Into g = Group + End Function +End Class +", dotAwait:=True, dotAwaitf:=True) + End Function + + + Public Async Function DotAwaitNotAfterConditionalAccessOfTaskMembers() As Task + Await VerifyNoItemsExistAsync(" +Imports System.Threading.Tasks + +Class C + Private Async Function F(ByVal someTask As Task) As Task + someTask?.$$ + End Function +End Class +") + End Function + + + + + + + + + + + + + + Public Async Function DotAwaitNotAfterDotInConditionalAccessChain(ByVal conditionalAccess As String) As Task + Await VerifyNoItemsExistAsync($" +Imports System.Threading.Tasks + +Public Class C + Public ReadOnly Property SomeTask As Task + Get + Return Task.CompletedTask + End Get + End Property + + Public ReadOnly Property Pro As C + Get + Return Me + End Get + End Property + + Public Function M() As C + Return Me + End Function +End Class + +Module Program + Async Function Main() As Task + Dim c = New C() + + If True Then + {conditionalAccess} + End If + End Function +End Module +") + End Function End Class End Namespace diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/AwaitCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/AwaitCompletionProvider.cs index 4825d121acdfa..8b295ea792da8 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/AwaitCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/AwaitCompletionProvider.cs @@ -5,12 +5,16 @@ using System; using System.Collections.Immutable; using System.Composition; +using System.Linq; +using System.Threading; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.LanguageServices; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers @@ -23,6 +27,7 @@ internal sealed class AwaitCompletionProvider : AbstractAwaitCompletionProvider [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public AwaitCompletionProvider() + : base(CSharpSyntaxFacts.Instance) { } @@ -31,7 +36,7 @@ public AwaitCompletionProvider() /// /// Gets the span start where async keyword should go. /// - private protected override int GetSpanStart(SyntaxNode declaration) + protected override int GetSpanStart(SyntaxNode declaration) { return declaration switch { @@ -47,22 +52,65 @@ private protected override int GetSpanStart(SyntaxNode declaration) }; } - private protected override SyntaxNode? GetAsyncSupportingDeclaration(SyntaxToken token) + protected override SyntaxNode? GetAsyncSupportingDeclaration(SyntaxToken token) { - var node = token.GetAncestor(node => node.IsAsyncSupportingFunctionSyntax()); - // For local functions, make sure we either have arrow token or open brace token. - // Consider the following scenario: - // void Method() - // { - // aw$$ AnotherMethodCall(); // NOTE: Here, the compiler produces LocalFunctionStatementSyntax. - // } - // For this case, we're interested in putting async in front of Method() - if (node is LocalFunctionStatementSyntax { ExpressionBody: null, Body: null }) + // In a case like + // someTask.$$ + // await Test(); + // someTask.await Test() is parsed as a local function statement. + // We skip this and look further up in the hierarchy. + var parent = token.Parent; + if (parent == null) + return null; + + if (parent is QualifiedNameSyntax { Parent: LocalFunctionStatementSyntax localFunction } qualifiedName && + localFunction.ReturnType == qualifiedName) { - return node.Parent?.FirstAncestorOrSelf(node => node.IsAsyncSupportingFunctionSyntax()); + parent = localFunction; } - return node; + return parent.Ancestors().FirstOrDefault(node => node.IsAsyncSupportingFunctionSyntax()); + } + + protected override SyntaxNode? GetExpressionToPlaceAwaitInFrontOf(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + { + var dotToken = GetDotTokenLeftOfPosition(syntaxTree, position, cancellationToken); + return dotToken?.Parent switch + { + // Don't support conditional access someTask?.$$ or c?.TaskReturning().$$ because there is no good completion until + // await? is supported by the language https://github.com/dotnet/csharplang/issues/35 + MemberAccessExpressionSyntax memberAccess => memberAccess.GetParentConditionalAccessExpression() is null ? memberAccess : null, + // someTask.$$. + RangeExpressionSyntax range => range.LeftOperand, + // special cases, where parsing is misleading. Such cases are handled in GetTypeSymbolOfExpression. + QualifiedNameSyntax qualifiedName => qualifiedName.Left, + _ => null, + }; + } + + protected override SyntaxToken? GetDotTokenLeftOfPosition(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + => CompletionUtilities.GetDotTokenLeftOfPosition(syntaxTree, position, cancellationToken); + + protected override ITypeSymbol? GetTypeSymbolOfExpression(SemanticModel semanticModel, SyntaxNode potentialAwaitableExpression, CancellationToken cancellationToken) + { + if (potentialAwaitableExpression is MemberAccessExpressionSyntax memberAccess) + { + var memberAccessExpression = memberAccess.Expression.WalkDownParentheses(); + // In cases like Task.$$ semanticModel.GetTypeInfo returns Task, but + // we don't want to suggest await here. We look up the symbol of the "Task" part + // and return null if it is a NamedType. + var symbol = semanticModel.GetSymbolInfo(memberAccessExpression, cancellationToken).Symbol; + return symbol is ITypeSymbol ? null : semanticModel.GetTypeInfo(memberAccessExpression, cancellationToken).Type; + } + else if (potentialAwaitableExpression is ExpressionSyntax expression && + expression.ShouldNameExpressionBeTreatedAsExpressionInsteadOfType(semanticModel, out _, out var container)) + { + return container; + } + else + { + return semanticModel.GetTypeInfo(potentialAwaitableExpression, cancellationToken).Type; + } } } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/CompletionUtilities.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/CompletionUtilities.cs index eadd8f3634996..bb28b5def1de1 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/CompletionUtilities.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/CompletionUtilities.cs @@ -5,6 +5,7 @@ #nullable disable using System.Collections.Immutable; +using System.Threading; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -41,6 +42,22 @@ public static bool TreatAsDot(SyntaxToken token, int characterPosition) return false; } + public static SyntaxToken? GetDotTokenLeftOfPosition(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) + { + var tokenOnLeft = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken, includeSkipped: true); + var dotToken = tokenOnLeft.GetPreviousTokenIfTouchingWord(position); + + // Has to be a . or a .. token + if (!TreatAsDot(dotToken, position - 1)) + return null; + + // don't want to trigger after a number. All other cases after dot are ok. + if (dotToken.GetPreviousToken().Kind() == SyntaxKind.NumericLiteralToken) + return null; + + return dotToken; + } + internal static bool IsTriggerCharacter(SourceText text, int characterPosition, OptionSet options) { var ch = text[characterPosition]; diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider.cs index 9446f42e90ddb..31f93219fdee4 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider.cs @@ -73,17 +73,9 @@ private static string SortText(int sortingGroupIndex, string sortTextSymbolPart) /// /// Gets the dot-like token we're after, and also the start of the expression we'd want to place any text before. /// - private static (SyntaxToken dotLikeToken, int expressionStart) GetDotAndExpressionStart(SyntaxNode root, int position) + private static (SyntaxToken dotLikeToken, int expressionStart) GetDotAndExpressionStart(SyntaxNode root, int position, CancellationToken cancellationToken) { - var tokenOnLeft = root.FindTokenOnLeftOfPosition(position, includeSkipped: true); - var dotToken = tokenOnLeft.GetPreviousTokenIfTouchingWord(position); - - // Has to be a . or a .. token - if (!CompletionUtilities.TreatAsDot(dotToken, position - 1)) - return default; - - // don't want to trigger after a number. All other cases after dot are ok. - if (dotToken.GetPreviousToken().Kind() == SyntaxKind.NumericLiteralToken) + if (CompletionUtilities.GetDotTokenLeftOfPosition(root.SyntaxTree, position, cancellationToken) is not SyntaxToken dotToken) return default; // if we have `.Name`, we want to get the parent member-access of that to find the starting position. @@ -111,7 +103,7 @@ public override async Task ProvideCompletionsAsync(CompletionContext context) return; var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var dotAndExprStart = GetDotAndExpressionStart(root, position); + var dotAndExprStart = GetDotAndExpressionStart(root, position, cancellationToken); if (dotAndExprStart == default) return; @@ -190,7 +182,7 @@ private static async Task ReplaceTextAfterOperatorAsync( var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var position = SymbolCompletionItem.GetContextPosition(item); - var (dotToken, _) = GetDotAndExpressionStart(root, position); + var (dotToken, _) = GetDotAndExpressionStart(root, position, cancellationToken); var questionToken = dotToken.GetPreviousToken().Kind() == SyntaxKind.QuestionToken ? dotToken.GetPreviousToken() : (SyntaxToken?)null; diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Conversions.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Conversions.cs index cdb6162ec51f2..4301a851975bc 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Conversions.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Conversions.cs @@ -72,7 +72,7 @@ private static async Task GetConversionChangeAsync( var position = SymbolCompletionItem.GetContextPosition(item); var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var (dotToken, _) = GetDotAndExpressionStart(root, position); + var (dotToken, _) = GetDotAndExpressionStart(root, position, cancellationToken); var questionToken = dotToken.GetPreviousToken().Kind() == SyntaxKind.QuestionToken ? dotToken.GetPreviousToken() diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Operators.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Operators.cs index 4008c39cec955..9691ebaae991c 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Operators.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/OperatorsAndIndexer/UnnamedSymbolCompletionProvider_Operators.cs @@ -133,7 +133,7 @@ private async Task GetOperatorChangeAsync( { var position = SymbolCompletionItem.GetContextPosition(item); var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var (dotLikeToken, expressionStart) = GetDotAndExpressionStart(root, position); + var (dotLikeToken, expressionStart) = GetDotAndExpressionStart(root, position, cancellationToken); // Place the new operator before the expression, and delete the dot. var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractAwaitCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractAwaitCompletionProvider.cs index 5f76e2d450eb1..f31fd9ee332f5 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractAwaitCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractAwaitCompletionProvider.cs @@ -2,6 +2,8 @@ // 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; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Threading; @@ -12,6 +14,7 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Completion.Providers { @@ -21,12 +24,56 @@ namespace Microsoft.CodeAnalysis.Completion.Providers /// internal abstract class AbstractAwaitCompletionProvider : LSPCompletionProvider { + private const string AwaitCompletionTargetTokenPosition = nameof(AwaitCompletionTargetTokenPosition); + private const string AppendConfigureAwait = nameof(AppendConfigureAwait); + private const string MakeContainerAsync = nameof(MakeContainerAsync); + + /// + /// If 'await' should be placed at the current position. If not present, it means to add 'await' prior + /// to the preceding expression. + /// + private const string AddAwaitAtCurrentPosition = nameof(AddAwaitAtCurrentPosition); + + protected enum DotAwaitContext + { + None, + AwaitOnly, + AwaitAndConfigureAwait, + } + + private readonly string _awaitKeyword; + private readonly string _awaitfDisplayText; + private readonly string _awaitfFilterText; + private readonly string _falseKeyword; + + protected AbstractAwaitCompletionProvider(ISyntaxFacts syntaxFacts) + { + _falseKeyword = syntaxFacts.GetText(syntaxFacts.SyntaxKinds.FalseKeyword); + _awaitKeyword = syntaxFacts.GetText(syntaxFacts.SyntaxKinds.AwaitKeyword); + _awaitfDisplayText = $"{_awaitKeyword}f"; + _awaitfFilterText = $"{_awaitKeyword}F"; // Uppercase F to select "awaitf" if "af" is written. + } + /// /// Gets the span start where async keyword should go. /// - private protected abstract int GetSpanStart(SyntaxNode declaration); + protected abstract int GetSpanStart(SyntaxNode declaration); - private protected abstract SyntaxNode? GetAsyncSupportingDeclaration(SyntaxToken token); + protected abstract SyntaxNode? GetAsyncSupportingDeclaration(SyntaxToken token); + + protected abstract ITypeSymbol? GetTypeSymbolOfExpression(SemanticModel semanticModel, SyntaxNode potentialAwaitableExpression, CancellationToken cancellationToken); + protected abstract SyntaxNode? GetExpressionToPlaceAwaitInFrontOf(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); + protected abstract SyntaxToken? GetDotTokenLeftOfPosition(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); + + private static bool IsConfigureAwaitable(Compilation compilation, ITypeSymbol symbol) + { + var originalDefinition = symbol.OriginalDefinition; + return + originalDefinition.Equals(compilation.TaskOfTType()) || + originalDefinition.Equals(compilation.TaskType()) || + originalDefinition.Equals(compilation.ValueTaskOfTType()) || + originalDefinition.Equals(compilation.ValueTaskType()); + } public sealed override async Task ProvideCompletionsAsync(CompletionContext context) { @@ -34,75 +81,181 @@ public sealed override async Task ProvideCompletionsAsync(CompletionContext cont var position = context.Position; var cancellationToken = context.CancellationToken; var syntaxFacts = document.GetRequiredLanguageService(); - var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); if (syntaxFacts.IsInNonUserCode(syntaxTree, position, cancellationToken)) - { return; - } var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); var syntaxContext = document.GetRequiredLanguageService().CreateContext(document, semanticModel, position, cancellationToken); - if (!syntaxContext.IsAwaitKeywordContext()) - { + + var isAwaitKeywordContext = syntaxContext.IsAwaitKeywordContext(); + var dotAwaitContext = GetDotAwaitKeywordContext(syntaxContext, cancellationToken); + if (!isAwaitKeywordContext && dotAwaitContext == DotAwaitContext.None) return; + + var token = syntaxContext.TargetToken; + var declaration = GetAsyncSupportingDeclaration(token); + + var properties = ImmutableDictionary.Empty + .Add(AwaitCompletionTargetTokenPosition, token.SpanStart.ToString()); + + var makeContainerAsync = declaration is not null && !SyntaxGenerator.GetGenerator(document).GetModifiers(declaration).IsAsync; + if (makeContainerAsync) + properties = properties.Add(MakeContainerAsync, string.Empty); + + if (isAwaitKeywordContext) + { + properties = properties.Add(AddAwaitAtCurrentPosition, string.Empty); + context.AddItem(CreateCompletionItem( + properties, _awaitKeyword, _awaitKeyword, + FeaturesResources.Asynchronously_waits_for_the_task_to_finish, + isComplexTextEdit: makeContainerAsync)); + } + else + { + Contract.ThrowIfTrue(dotAwaitContext == DotAwaitContext.None); + + // add the `await` option that will remove the dot and add `await` to the start of the expression. + context.AddItem(CreateCompletionItem( + properties, _awaitKeyword, _awaitKeyword, + FeaturesResources.Await_the_preceding_expression, + isComplexTextEdit: true)); + + if (dotAwaitContext == DotAwaitContext.AwaitAndConfigureAwait) + { + // add the `awaitf` option to do the same, but also add .ConfigureAwait(false); + properties = properties.Add(AppendConfigureAwait, string.Empty); + context.AddItem(CreateCompletionItem( + properties, _awaitfDisplayText, _awaitfFilterText, + string.Format(FeaturesResources.Await_the_preceding_expression_and_add_ConfigureAwait_0, _falseKeyword), + isComplexTextEdit: true)); + } } - var generator = SyntaxGenerator.GetGenerator(document); - var syntaxKinds = document.GetRequiredLanguageService(); - var completionItem = GetCompletionItem(syntaxContext.TargetToken, generator, syntaxKinds, syntaxFacts); - context.AddItem(completionItem); + return; + + static CompletionItem CreateCompletionItem( + ImmutableDictionary completionProperties, string displayText, string filterText, string tooltip, bool isComplexTextEdit) + { + var appendConfigureAwait = completionProperties.ContainsKey(AppendConfigureAwait); + + var description = appendConfigureAwait + ? ImmutableArray.Create(new SymbolDisplayPart(SymbolDisplayPartKind.Text, null, tooltip)) + : RecommendedKeyword.CreateDisplayParts(displayText, tooltip); + + return CommonCompletionItem.Create( + displayText: displayText, + displayTextSuffix: "", + filterText: filterText, + rules: CompletionItemRules.Default, + glyph: Glyph.Keyword, + description: description, + isComplexTextEdit: isComplexTextEdit, + properties: completionProperties); + } } - public sealed override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) + public sealed override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) { - // IsComplexTextEdit is true when we want to add async to the container. + // IsComplexTextEdit is true when we want to add async to the container or place await in front of the expression. if (!item.IsComplexTextEdit) - { return await base.GetChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false); + + using var _ = ArrayBuilder.GetInstance(out var builder); + + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + var syntaxKinds = syntaxFacts.SyntaxKinds; + var properties = item.Properties; + + if (properties.ContainsKey(MakeContainerAsync)) + { + var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var tokenPosition = int.Parse(properties[AwaitCompletionTargetTokenPosition]); + var declaration = GetAsyncSupportingDeclaration(root.FindToken(tokenPosition)); + if (declaration is null) + { + // IsComplexTextEdit should only be true when GetAsyncSupportingDeclaration returns non-null. + // This is ensured by the ShouldMakeContainerAsync overrides. + Debug.Fail("Expected non-null value for declaration."); + return await base.GetChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false); + } + + builder.Add(new TextChange(new TextSpan(GetSpanStart(declaration), 0), syntaxFacts.GetText(syntaxKinds.AsyncKeyword) + " ")); } - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var declaration = GetAsyncSupportingDeclaration(root.FindToken(item.Span.Start)); - if (declaration is null) + if (properties.ContainsKey(AddAwaitAtCurrentPosition)) { - // IsComplexTextEdit should only be true when GetAsyncSupportingDeclaration returns non-null. - // This is ensured by the ShouldMakeContainerAsync overrides. - Debug.Assert(false, "Expected non-null value for declaration."); - return await base.GetChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false); + builder.Add(new TextChange(item.Span, _awaitKeyword)); } + else + { + var position = item.Span.Start; + var dotToken = GetDotTokenLeftOfPosition(syntaxTree, position, cancellationToken); + var expr = GetExpressionToPlaceAwaitInFrontOf(syntaxTree, position, cancellationToken); - var syntaxFacts = document.GetRequiredLanguageService(); - var syntaxKinds = document.GetRequiredLanguageService(); + Contract.ThrowIfFalse(dotToken.HasValue); + Contract.ThrowIfNull(expr); - using var _ = ArrayBuilder.GetInstance(out var builder); - builder.Add(new TextChange(new TextSpan(GetSpanStart(declaration), 0), syntaxFacts.GetText(syntaxKinds.AsyncKeyword) + " ")); - builder.Add(new TextChange(item.Span, item.DisplayText)); + // place "await" in front of expr + builder.Add(new TextChange(new TextSpan(expr.SpanStart, 0), _awaitKeyword + " ")); + + // remove any text after dot, including the dot token and optionally append .ConfigureAwait(false) + var replacementText = properties.ContainsKey(AppendConfigureAwait) + ? $".{nameof(Task.ConfigureAwait)}({_falseKeyword})" + : ""; + + builder.Add(new TextChange(TextSpan.FromBounds(dotToken.Value.SpanStart, item.Span.End), replacementText)); + } var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); var newText = text.WithChanges(builder); return CompletionChange.Create(Utilities.Collapse(newText, builder.ToImmutableArray())); } -#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods - private protected bool ShouldMakeContainerAsync(SyntaxToken token, SyntaxGenerator generator) -#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods + /// + /// Should be offered, if left of the dot at position is an awaitable expression? + /// + /// someTask.$$ // Suggest await completion + /// await someTask.$$ // Don't suggest await completion + /// + /// + /// + /// , if await can not be suggested for the expression left of the dot. + /// , if await should be suggested for the expression left of the dot, but ConfigureAwait(false) not. + /// , if await should be suggested for the expression left of the dot and ConfigureAwait(false). + /// + private DotAwaitContext GetDotAwaitKeywordContext(SyntaxContext syntaxContext, CancellationToken cancellationToken) { - var declaration = GetAsyncSupportingDeclaration(token); - return declaration is not null && !generator.GetModifiers(declaration).IsAsync; - } + var position = syntaxContext.Position; + var syntaxTree = syntaxContext.SyntaxTree; + var potentialAwaitableExpression = GetExpressionToPlaceAwaitInFrontOf(syntaxTree, position, cancellationToken); + if (potentialAwaitableExpression is not null) + { + var parentOfAwaitable = potentialAwaitableExpression.Parent; + var document = syntaxContext.Document; + var syntaxFacts = document.GetRequiredLanguageService(); + if (!syntaxFacts.IsAwaitExpression(parentOfAwaitable)) + { + var semanticModel = syntaxContext.SemanticModel; + var symbol = GetTypeSymbolOfExpression(semanticModel, potentialAwaitableExpression, cancellationToken); + if (symbol.IsAwaitableNonDynamic(semanticModel, position)) + { + // We have a awaitable type left of the dot, that is not yet awaited. + // We need to check if await is valid at the insertion position. + var syntaxContextAtInsertationPosition = syntaxContext.GetLanguageService().CreateContext( + document, syntaxContext.SemanticModel, potentialAwaitableExpression.SpanStart, cancellationToken); + if (syntaxContextAtInsertationPosition.IsAwaitKeywordContext()) + { + return IsConfigureAwaitable(syntaxContext.SemanticModel.Compilation, symbol) + ? DotAwaitContext.AwaitAndConfigureAwait + : DotAwaitContext.AwaitOnly; + } + } + } + } - private CompletionItem GetCompletionItem(SyntaxToken token, SyntaxGenerator generator, ISyntaxKindsService syntaxKinds, ISyntaxFactsService syntaxFacts) - { - var shouldMakeContainerAsync = ShouldMakeContainerAsync(token, generator); - var text = syntaxFacts.GetText(syntaxKinds.AwaitKeyword); - return CommonCompletionItem.Create( - displayText: text, - displayTextSuffix: "", - rules: CompletionItemRules.Default, - Glyph.Keyword, - description: RecommendedKeyword.CreateDisplayParts(text, FeaturesResources.Asynchronously_waits_for_the_task_to_finish), - inlineDescription: shouldMakeContainerAsync ? FeaturesResources.Make_containing_scope_async : null, - isComplexTextEdit: shouldMakeContainerAsync); + return DotAwaitContext.None; } } } diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index b9553cd31fb3a..f96d0ebd7b71b 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -965,11 +965,12 @@ This version used in: {2} Asynchronously waits for the task to finish. - - Make containing scope async + + Await the preceding expression - - Make containing scope async (return Task) + + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. (Unknown) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf index 94e3b152f5413..54594cfb92b36 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf @@ -240,6 +240,16 @@ Ujistěte se, že specifikátor tt použijete pro jazyky, pro které je nezbytn Asynchronně čeká na dokončení úlohy. + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' Očekávaná úloha vrací {0}. @@ -4175,16 +4185,6 @@ Tato verze se používá zde: {2}. Přidat chybějící uzly parametrů - - Make containing scope async - Převést obsažený obor na asynchronní - - - - Make containing scope async (return Task) - Převést obsažený obor na asynchronní (návratová hodnota Task) - - (Unknown) (neznámé) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf index 70eceec3ba9d8..f139a0035f8d9 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf @@ -240,6 +240,16 @@ Stellen Sie sicher, dass Sie den Bezeichner "tt" für Sprachen verwenden, für d Wartet asynchron darauf, dass die Aufgabe fertig ist. + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' Erwartete Aufgabe gibt "{0}" zurück @@ -4175,16 +4185,6 @@ Diese Version wird verwendet in: {2} Fehlende Parameterknoten hinzufügen - - Make containing scope async - Enthaltenden Bereich als asynchron definieren - - - - Make containing scope async (return Task) - Enthaltenden Bereich als asynchron definieren (Task zurückgeben) - - (Unknown) (Unbekannt) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf index e43c0d9087606..1119fbbfc4f04 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf @@ -240,6 +240,16 @@ Asegúrese de usar el especificador "tt" para los idiomas para los que es necesa Espera de forma asincrónica a que termine la tarea. + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' La tarea esperada devuelve "{0}". @@ -4175,16 +4185,6 @@ Esta versión se utiliza en: {2} Agregar nodos de parámetros que faltan - - Make containing scope async - Convertir el ámbito contenedor en asincrónico - - - - Make containing scope async (return Task) - Convertir el ámbito contenedor en asincrónico (devolver Task) - - (Unknown) (Desconocido) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf index b1774a1c29b57..08aa244b01c6e 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf @@ -240,6 +240,16 @@ Veillez à utiliser le spécificateur "tt" pour les langues où il est nécessai Attend de façon asynchrone que la tâche soit terminée. + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' La tâche attendue retourne '{0}' @@ -4175,16 +4185,6 @@ Version utilisée dans : {2} Ajouter les nœuds de paramètre manquants - - Make containing scope async - Rendre la portée contenante async - - - - Make containing scope async (return Task) - Rendre la portée contenante async (retourner Task) - - (Unknown) (Inconnu) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf index e0487be1a9e7d..4b65ef9dfc22c 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf @@ -240,6 +240,16 @@ Assicurarsi di usare l'identificatore "tt" per le lingue per le quali è necessa Attende in modalità asincrona il completamento dell'attività. + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' L'attività attesa restituisce '{0}' @@ -4175,16 +4185,6 @@ Questa versione è usata {2} Aggiungi nodi di parametro mancanti - - Make containing scope async - Rendi asincrono l'ambito contenitore - - - - Make containing scope async (return Task) - Rendi asincrono l'ambito contenitore (restituisci Task) - - (Unknown) (Sconosciuto) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf index 3736a7fd7ea8c..8eece4fa99595 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf @@ -240,6 +240,16 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma タスクの完了を非同期に待機します。 + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' 待機中のタスクから '{0}' が返されました @@ -4175,16 +4185,6 @@ This version used in: {2} 欠落している Param ノードを追加する - - Make containing scope async - 含まれているスコープを非同期にします - - - - Make containing scope async (return Task) - 含まれているスコープを非同期にします (Task を返します) - - (Unknown) (不明) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf index 108ade860a862..5e6184be8a065 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf @@ -240,6 +240,16 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma 작업이 완료될 때까지 비동기적으로 기다립니다. + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' 대기된 작업에서 '{0}'이(가) 반환됨 @@ -4175,16 +4185,6 @@ This version used in: {2} 누락된 매개 변수 노드 추가 - - Make containing scope async - 포함 범위를 비동기로 설정 - - - - Make containing scope async (return Task) - 포함 범위를 비동기로 설정(Task 반환) - - (Unknown) (알 수 없음) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf index 8f0cc37c0a3ee..1190c5ace986f 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf @@ -240,6 +240,16 @@ Pamiętaj, aby nie używać specyfikatora „tt” dla wszystkich języków, w k Asynchronicznie oczekuje na zakończenie zadania. + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' Zadanie, na które oczekiwano, zwraca wartość „{0}” @@ -4175,16 +4185,6 @@ Ta wersja jest używana wersja: {2} Dodaj brakujące węzły parametrów - - Make containing scope async - Ustaw zawierający zakres jako asynchroniczny - - - - Make containing scope async (return Task) - Ustaw zawierający zakres jako asynchroniczny (zwróć typ Task) - - (Unknown) (Nieznany) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf index cbc7450451337..c39320711f3e9 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf @@ -240,6 +240,16 @@ Verifique se o especificador "tt" foi usado para idiomas para os quais é necess Assincronamente aguarda a conclusão da tarefa. + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' A tarefa esperada retorna '{0}' @@ -4175,16 +4185,6 @@ Essa versão é usada no: {2} Adicionar nós de parâmetro ausentes - - Make containing scope async - Tornar o escopo contentor assíncrono - - - - Make containing scope async (return Task) - Tornar o escopo contentor assíncrono (retornar Task) - - (Unknown) (Desconhecido) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf index 471724a8d8396..667fc3ff18c60 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf @@ -240,6 +240,16 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma Асинхронно ожидает окончания задачи. + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' Ожидаемая задача возвращает "{0}". @@ -4175,16 +4185,6 @@ This version used in: {2} Добавить отсутствующие узлы параметров - - Make containing scope async - Преобразовать содержащую область в асинхронную - - - - Make containing scope async (return Task) - Преобразовать содержащую область в асинхронную (задача возврата) - - (Unknown) (Неизвестно) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf index 25eb5f90cdb81..ea8331e11fbc7 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf @@ -240,6 +240,16 @@ AM ve PM arasındaki farkın korunmasının gerekli olduğu diller için "tt" be Zaman uyumsuz olarak görevin tamamlanmasını bekler. + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' Beklenen görev '{0}' döndürüyor @@ -4175,16 +4185,6 @@ Bu sürüm şurada kullanılır: {2} Eksik parametre düğümlerini ekle - - Make containing scope async - İçeren kapsamı asenkron yap - - - - Make containing scope async (return Task) - İçeren kapsamı asenkron yap (Görevi döndür) - - (Unknown) (Bilinmiyor) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf index bcb2e26309ace..cbf7746ad9acd 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf @@ -240,6 +240,16 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma 异步等待任务完成。 + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' 等待任务返回“{0}” @@ -4175,16 +4185,6 @@ This version used in: {2} 添加缺少的参数节点 - - Make containing scope async - 将包含范围改为 Async - - - - Make containing scope async (return Task) - 将包含范围改为 Async (返回“任务”) - - (Unknown) (未知) diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf index 3f3400f1f66a9..914552bf82edf 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf @@ -240,6 +240,16 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma 以非同步方式等候工作完成。 + + Await the preceding expression + Await the preceding expression + + + + Await the preceding expression and add ConfigureAwait({0}). + Await the preceding expression and add ConfigureAwait({0}). + {Locked="ConfigureAwait"} "ConfigureAwait" is an api name and should not be localized. {0} is a placeholder for the language specific keyword 'false'. + Awaited task returns '{0}' 等待的工作會傳回 '{0}' @@ -4175,16 +4185,6 @@ This version used in: {2} 新增遺失的參數節點 - - Make containing scope async - 讓包含範圍非同步 - - - - Make containing scope async (return Task) - 讓包含範圍非同步 (傳回工作) - - (Unknown) (未知) diff --git a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/AwaitCompletionProvider.vb b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/AwaitCompletionProvider.vb index 8e9300425ff0a..a1beadfd99fd0 100644 --- a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/AwaitCompletionProvider.vb +++ b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/AwaitCompletionProvider.vb @@ -4,10 +4,11 @@ Imports System.Collections.Immutable Imports System.Composition +Imports System.Threading Imports Microsoft.CodeAnalysis.Completion Imports Microsoft.CodeAnalysis.Completion.Providers Imports Microsoft.CodeAnalysis.Host.Mef -Imports Microsoft.CodeAnalysis.VisualBasic.CodeGeneration +Imports Microsoft.CodeAnalysis.VisualBasic.LanguageServices Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers @@ -20,11 +21,12 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers Public Sub New() + MyBase.New(VisualBasicSyntaxFacts.Instance) End Sub Public Overrides ReadOnly Property TriggerCharacters As ImmutableHashSet(Of Char) = CommonTriggerChars - Private Protected Overrides Function GetSpanStart(declaration As SyntaxNode) As Integer + Protected Overrides Function GetSpanStart(declaration As SyntaxNode) As Integer Select Case declaration.Kind() Case SyntaxKind.FunctionBlock, SyntaxKind.SubBlock @@ -39,8 +41,50 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers Throw ExceptionUtilities.Unreachable End Function - Private Protected Overrides Function GetAsyncSupportingDeclaration(token As SyntaxToken) As SyntaxNode + Protected Overrides Function GetAsyncSupportingDeclaration(token As SyntaxToken) As SyntaxNode Return token.GetAncestor(Function(node) node.IsAsyncSupportedFunctionSyntax()) End Function + + Protected Overrides Function GetTypeSymbolOfExpression(semanticModel As SemanticModel, potentialAwaitableExpression As SyntaxNode, cancellationToken As CancellationToken) As ITypeSymbol + Dim memberAccessExpression = TryCast(potentialAwaitableExpression, MemberAccessExpressionSyntax)?.Expression + If memberAccessExpression Is Nothing Then + Return Nothing + End If + + Dim symbol = semanticModel.GetSymbolInfo(memberAccessExpression.WalkDownParentheses(), cancellationToken).Symbol + Return If(TypeOf symbol Is ITypeSymbol, Nothing, semanticModel.GetTypeInfo(memberAccessExpression, cancellationToken).Type) + End Function + + Protected Overrides Function GetExpressionToPlaceAwaitInFrontOf(syntaxTree As SyntaxTree, position As Integer, cancellationToken As CancellationToken) As SyntaxNode + Dim dotToken = GetDotTokenLeftOfPosition(syntaxTree, position, cancellationToken) + If Not dotToken.HasValue Then + Return Nothing + End If + + Dim memberAccess = TryCast(dotToken.Value.Parent, MemberAccessExpressionSyntax) + If memberAccess Is Nothing Then + Return Nothing + End If + + If memberAccess.Expression.GetParentConditionalAccessExpression() IsNot Nothing Then + Return Nothing + End If + + Return memberAccess + End Function + + Protected Overrides Function GetDotTokenLeftOfPosition(syntaxTree As SyntaxTree, position As Integer, cancellationToken As CancellationToken) As SyntaxToken? + Dim tokenOnLeft = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken) + Dim dotToken = tokenOnLeft.GetPreviousTokenIfTouchingWord(position) + If Not dotToken.IsKind(SyntaxKind.DotToken) Then + Return Nothing + End If + + If dotToken.GetPreviousToken().IsKind(SyntaxKind.IntegerLiteralToken, SyntaxKind.FloatingLiteralToken, SyntaxKind.DecimalLiteralToken, SyntaxKind.DateLiteralToken) Then + Return Nothing + End If + + Return dotToken + End Function End Class End Namespace diff --git a/src/Workspaces/CSharp/Portable/Recommendations/CSharpRecommendationServiceRunner.cs b/src/Workspaces/CSharp/Portable/Recommendations/CSharpRecommendationServiceRunner.cs index d7d2d3ec3ecb0..f3ea5dcf6182b 100644 --- a/src/Workspaces/CSharp/Portable/Recommendations/CSharpRecommendationServiceRunner.cs +++ b/src/Workspaces/CSharp/Portable/Recommendations/CSharpRecommendationServiceRunner.cs @@ -253,7 +253,7 @@ private RecommendedSymbols GetSymbolsOffOfName(NameSyntax name) if (_context.IsEnumTypeMemberAccessContext) return GetSymbolsOffOfExpression(name); - if (ShouldBeTreatedAsTypeInsteadOfExpression(name, out var nameBinding, out var container)) + if (name.ShouldNameExpressionBeTreatedAsExpressionInsteadOfType(_context.SemanticModel, out var nameBinding, out var container)) return GetSymbolsOffOfBoundExpression(name, name, nameBinding, container, unwrapNullable: false); // We're in a name-only context, since if we were an expression we'd be a @@ -293,46 +293,6 @@ private RecommendedSymbols GetSymbolsOffOfName(NameSyntax name) return new RecommendedSymbols(symbols); } - /// - /// DeterminesCheck if we're in an interesting situation like this: - /// - /// int i = 5; - /// i. // -- here - /// List ml = new List(); - /// - /// The problem is that "i.List" gets parsed as a type. In this case we need to try binding again as if "i" is - /// an expression and not a type. In order to do that, we need to speculate as to what 'i' meant if it wasn't - /// part of a local declaration's type. - /// - /// Another interesting case is something like: - /// - /// stringList. - /// await Test2(); - /// - /// Here "stringList.await" is thought of as the return type of a local function. - /// - private bool ShouldBeTreatedAsTypeInsteadOfExpression( - ExpressionSyntax name, - out SymbolInfo leftHandBinding, - out ITypeSymbol? container) - { - if (name.IsFoundUnder(d => d.ReturnType) || - name.IsFoundUnder(d => d.Declaration.Type) || - name.IsFoundUnder(d => d.Declaration.Type)) - { - leftHandBinding = _context.SemanticModel.GetSpeculativeSymbolInfo( - name.SpanStart, name, SpeculativeBindingOption.BindAsExpression); - - container = _context.SemanticModel.GetSpeculativeTypeInfo( - name.SpanStart, name, SpeculativeBindingOption.BindAsExpression).Type; - return true; - } - - leftHandBinding = default; - container = null; - return false; - } - private RecommendedSymbols GetSymbolsOffOfExpression(ExpressionSyntax? originalExpression) { if (originalExpression == null) @@ -527,7 +487,7 @@ private ImmutableArray GetUnnamedSymbols(ExpressionSyntax originalExpre private ITypeSymbol? GetContainerForUnnamedSymbols(SemanticModel semanticModel, ExpressionSyntax originalExpression) { - return ShouldBeTreatedAsTypeInsteadOfExpression(originalExpression, out _, out var container) + return originalExpression.ShouldNameExpressionBeTreatedAsExpressionInsteadOfType(_context.SemanticModel, out _, out var container) ? container : semanticModel.GetTypeInfo(originalExpression, _cancellationToken).Type; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxKinds.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxKinds.cs index 13402a07976e5..66bab6009ff2d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxKinds.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxKinds.cs @@ -38,6 +38,8 @@ public TSyntaxKind Convert(int kind) where TSyntaxKind : struct public int StringLiteralToken => (int)SyntaxKind.StringLiteralToken; public int IfKeyword => (int)SyntaxKind.IfKeyword; + public int TrueKeyword => (int)SyntaxKind.TrueKeyword; + public int FalseKeyword => (int)SyntaxKind.FalseKeyword; public int GenericName => (int)SyntaxKind.GenericName; public int IdentifierName => (int)SyntaxKind.IdentifierName; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxKinds.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxKinds.cs index ef64a33e9d064..b66e46deda84d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxKinds.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxKinds.cs @@ -43,6 +43,8 @@ internal interface ISyntaxKinds int GlobalKeyword { get; } int IfKeyword { get; } int? GlobalStatement { get; } + int TrueKeyword { get; } + int FalseKeyword { get; } #endregion diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxKinds.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxKinds.vb index a6d25bf9a8ab3..94ab0cd2a79dd 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxKinds.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxKinds.vb @@ -39,6 +39,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageServices Public ReadOnly Property StringLiteralToken As Integer = SyntaxKind.StringLiteralToken Implements ISyntaxKinds.StringLiteralToken Public ReadOnly Property IfKeyword As Integer = SyntaxKind.IfKeyword Implements ISyntaxKinds.IfKeyword + Public ReadOnly Property TrueKeyword As Integer = SyntaxKind.TrueKeyword Implements ISyntaxKinds.TrueKeyword + Public ReadOnly Property FalseKeyword As Integer = SyntaxKind.FalseKeyword Implements ISyntaxKinds.FalseKeyword Public ReadOnly Property GenericName As Integer = SyntaxKind.GenericName Implements ISyntaxKinds.GenericName Public ReadOnly Property IdentifierName As Integer = SyntaxKind.IdentifierName Implements ISyntaxKinds.IdentifierName diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/CSharpSyntaxContext.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/CSharpSyntaxContext.cs index dd7709b500df0..a7024465f96ed 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/CSharpSyntaxContext.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/CSharpSyntaxContext.cs @@ -427,6 +427,18 @@ internal override bool IsAwaitKeywordContext() if (node.IsKind(SyntaxKind.QueryExpression)) { + // There are some cases where "await" is allowed in a query context. See error CS1995 for details: + // error CS1995: The 'await' operator may only be used in a query expression within the first collection expression of the initial 'from' clause or within the collection expression of a 'join' clause + if (TargetToken.IsKind(SyntaxKind.InKeyword)) + { + return TargetToken.Parent switch + { + FromClauseSyntax { Parent: QueryExpressionSyntax queryExpression } fromClause => queryExpression.FromClause == fromClause, + JoinClauseSyntax => true, + _ => false, + }; + } + return false; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ExpressionSyntaxExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ExpressionSyntaxExtensions.cs index 9ee7a79a5eaf3..ae0ce54503def 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ExpressionSyntaxExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ExpressionSyntaxExtensions.cs @@ -143,6 +143,46 @@ public static ExpressionSyntax CastIfPossible( return castExpression; } + /// + /// DeterminesCheck if we're in an interesting situation like this: + /// + /// int i = 5; + /// i. // -- here + /// List ml = new List(); + /// + /// The problem is that "i.List" gets parsed as a type. In this case we need to try binding again as if "i" is + /// an expression and not a type. In order to do that, we need to speculate as to what 'i' meant if it wasn't + /// part of a local declaration's type. + /// + /// Another interesting case is something like: + /// + /// stringList. + /// await Test2(); + /// + /// Here "stringList.await" is thought of as the return type of a local function. + /// + public static bool ShouldNameExpressionBeTreatedAsExpressionInsteadOfType( + this ExpressionSyntax name, + SemanticModel semanticModel, + out SymbolInfo leftHandBinding, + out ITypeSymbol? container) + { + if (name.IsFoundUnder(d => d.ReturnType) || + name.IsFoundUnder(d => d.Declaration.Type) || + name.IsFoundUnder(d => d.Declaration.Type)) + { + leftHandBinding = semanticModel.GetSpeculativeSymbolInfo( + name.SpanStart, name, SpeculativeBindingOption.BindAsExpression); + + container = semanticModel.GetSpeculativeTypeInfo( + name.SpanStart, name, SpeculativeBindingOption.BindAsExpression).Type; + return true; + } + + leftHandBinding = default; + container = null; + return false; + } } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/Extensions/ContextQuery/VisualBasicSyntaxContext.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/Extensions/ContextQuery/VisualBasicSyntaxContext.vb index f0c97f08c2d36..ef924d640c569 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/Extensions/ContextQuery/VisualBasicSyntaxContext.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/Extensions/ContextQuery/VisualBasicSyntaxContext.vb @@ -170,9 +170,33 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions.ContextQuery Friend Overrides Function IsAwaitKeywordContext() As Boolean If IsAnyExpressionContext OrElse IsSingleLineStatementContext Then + If IsInQuery Then + ' There are some places where Await is allowed: + ' BC36929: 'Await' may only be used in a query expression within the first collection expression of the initial 'From' clause or within the collection expression of a 'Join' clause. + If TargetToken.Kind = SyntaxKind.InKeyword Then + Dim collectionRange = TryCast(TargetToken.Parent, CollectionRangeVariableSyntax) + If collectionRange IsNot Nothing Then + If TypeOf collectionRange.Parent Is FromClauseSyntax AndAlso TypeOf collectionRange.Parent.Parent Is QueryExpressionSyntax Then + Dim fromClause = DirectCast(collectionRange.Parent, FromClauseSyntax) + Dim queryExpression = DirectCast(collectionRange.Parent.Parent, QueryExpressionSyntax) + ' Await is only allowed for the first collection in a from clause. There are two forms to consider here: + ' 1. From x In xs From y In ys + ' 2. From x In xs, y In ys + ' 1. and 2. can be combined, but in any combination, Await is only allowed on the very first collection + If fromClause.Variables.FirstOrDefault() Is collectionRange AndAlso queryExpression.Clauses.FirstOrDefault() Is collectionRange.Parent Then + Return True + End If + ElseIf TypeOf collectionRange.Parent Is SimpleJoinClauseSyntax OrElse TypeOf collectionRange.Parent Is GroupJoinClauseSyntax Then + Return True + End If + End If + End If + + Return False + End If For Each node In TargetToken.GetAncestors(Of SyntaxNode)() If node.IsKind(SyntaxKind.SingleLineSubLambdaExpression, SyntaxKind.SingleLineFunctionLambdaExpression, - SyntaxKind.MultiLineSubLambdaExpression, SyntaxKind.MultiLineFunctionLambdaExpression) Then + SyntaxKind.MultiLineSubLambdaExpression, SyntaxKind.MultiLineFunctionLambdaExpression) Then Return True End If