diff --git a/src/Features/CSharp/Portable/Snippets/AbstractCSharpForLoopSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/AbstractCSharpForLoopSnippetProvider.cs index ac339ddcd4b0f..96e5075f2a99d 100644 --- a/src/Features/CSharp/Portable/Snippets/AbstractCSharpForLoopSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/AbstractCSharpForLoopSnippetProvider.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.LanguageService; @@ -38,6 +39,9 @@ internal abstract class AbstractCSharpForLoopSnippetProvider : AbstractForLoopSn protected abstract void AddSpecificPlaceholders(MultiDictionary placeholderBuilder, ExpressionSyntax initializer, ExpressionSyntax rightOfCondition); + protected override bool CanInsertStatementAfterToken(SyntaxToken token) + => token.IsBeginningOfStatementContext() || token.IsBeginningOfGlobalStatementContext(); + protected override ForStatementSyntax GenerateStatement(SyntaxGenerator generator, SyntaxContext syntaxContext, InlineExpressionInfo? inlineExpressionInfo) { var semanticModel = syntaxContext.SemanticModel; diff --git a/src/Features/CSharp/Portable/Snippets/CSharpDoWhileLoopSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpDoWhileLoopSnippetProvider.cs index e29b74da65358..6c98a8bd41729 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpDoWhileLoopSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpDoWhileLoopSnippetProvider.cs @@ -6,6 +6,7 @@ using System.Composition; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Host.Mef; @@ -26,6 +27,9 @@ internal sealed class CSharpDoWhileLoopSnippetProvider() public override string Description => CSharpFeaturesResources.do_while_loop; + protected override bool CanInsertStatementAfterToken(SyntaxToken token) + => token.IsBeginningOfStatementContext() || token.IsBeginningOfGlobalStatementContext(); + protected override DoStatementSyntax GenerateStatement(SyntaxGenerator generator, SyntaxContext syntaxContext, InlineExpressionInfo? inlineExpressionInfo) { return SyntaxFactory.DoStatement( diff --git a/src/Features/CSharp/Portable/Snippets/CSharpForEachLoopSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpForEachLoopSnippetProvider.cs index 32c64990213fc..e0c8b4e7a5dec 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpForEachLoopSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpForEachLoopSnippetProvider.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Host.Mef; @@ -51,6 +52,9 @@ protected override bool IsValidSnippetLocationCore(SnippetContext context, Cance return base.IsValidSnippetLocationCore(context, cancellationToken); } + protected override bool CanInsertStatementAfterToken(SyntaxToken token) + => token.IsBeginningOfStatementContext() || token.IsBeginningOfGlobalStatementContext(); + protected override ForEachStatementSyntax GenerateStatement(SyntaxGenerator generator, SyntaxContext syntaxContext, InlineExpressionInfo? inlineExpressionInfo) { var semanticModel = syntaxContext.SemanticModel; diff --git a/src/Features/CSharp/Portable/Snippets/CSharpIfSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpIfSnippetProvider.cs index a93cfd6468c69..e657d23918c07 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpIfSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpIfSnippetProvider.cs @@ -6,6 +6,7 @@ using System.Composition; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Snippets; @@ -23,6 +24,9 @@ internal sealed class CSharpIfSnippetProvider() : AbstractIfSnippetProvider FeaturesResources.if_statement; + protected override bool CanInsertStatementAfterToken(SyntaxToken token) + => token.IsBeginningOfStatementContext() || token.IsBeginningOfGlobalStatementContext(); + protected override ExpressionSyntax GetCondition(IfStatementSyntax node) => node.Condition; diff --git a/src/Features/CSharp/Portable/Snippets/CSharpWhileLoopSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpWhileLoopSnippetProvider.cs index 28121dccb802a..9884811b76bcc 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpWhileLoopSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpWhileLoopSnippetProvider.cs @@ -6,6 +6,7 @@ using System.Composition; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Snippets; @@ -23,6 +24,9 @@ internal sealed class CSharpWhileLoopSnippetProvider() : AbstractWhileLoopSnippe public override string Description => FeaturesResources.while_loop; + protected override bool CanInsertStatementAfterToken(SyntaxToken token) + => token.IsBeginningOfStatementContext() || token.IsBeginningOfGlobalStatementContext(); + protected override ExpressionSyntax GetCondition(WhileStatementSyntax node) => node.Condition; diff --git a/src/Features/CSharpTest/Snippets/AbstractCSharpConditionalBlockSnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/AbstractCSharpConditionalBlockSnippetProviderTests.cs index cc084154e6f48..1e17af2b5fce3 100644 --- a/src/Features/CSharpTest/Snippets/AbstractCSharpConditionalBlockSnippetProviderTests.cs +++ b/src/Features/CSharpTest/Snippets/AbstractCSharpConditionalBlockSnippetProviderTests.cs @@ -539,6 +539,65 @@ void M(bool flag) """); } + [Fact] + public async Task InsertInlineSnippetWhenDottingBeforeMemberAccessExpressionOnTheNextLineTest() + { + await VerifySnippetAsync(""" + using System; + + class C + { + void M(bool flag) + { + flag.$$ + Console.WriteLine(); + } + } + """, $$""" + using System; + + class C + { + void M(bool flag) + { + {{SnippetIdentifier}} (flag) + { + $$ + } + Console.WriteLine(); + } + } + """); + } + + [Fact] + public async Task NoInlineSnippetWhenDottingBeforeMemberAccessExpressionOnTheSameLineTest() + { + await VerifySnippetIsAbsentAsync(""" + class C + { + void M(bool flag) + { + flag.$$ToString(); + } + } + """); + } + + [Fact] + public async Task NoInlineSnippetWhenDottingBeforeContextualKeywordOnTheSameLineTest() + { + await VerifySnippetIsAbsentAsync(""" + class C + { + void M(bool flag) + { + flag.$$var a = 0; + } + } + """); + } + [Fact] public async Task NoInlineSnippetForTypeItselfTest() { diff --git a/src/Features/CSharpTest/Snippets/CSharpDoSnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/CSharpDoSnippetProviderTests.cs index fe6ee4921126b..2a183f2d66730 100644 --- a/src/Features/CSharpTest/Snippets/CSharpDoSnippetProviderTests.cs +++ b/src/Features/CSharpTest/Snippets/CSharpDoSnippetProviderTests.cs @@ -555,6 +555,66 @@ void M(bool flag) """); } + [Fact] + public async Task InsertInlineDoSnippetWhenDottingBeforeMemberAccessExpressionOnTheNextLineTest() + { + await VerifySnippetAsync(""" + using System; + + class C + { + void M(bool flag) + { + flag.$$ + Console.WriteLine(); + } + } + """, """ + using System; + + class C + { + void M(bool flag) + { + do + { + $$ + } + while (flag); + Console.WriteLine(); + } + } + """); + } + + [Fact] + public async Task NoInlineDoSnippetWhenDottingBeforeMemberAccessExpressionOnTheSameLineTest() + { + await VerifySnippetIsAbsentAsync(""" + class C + { + void M(bool flag) + { + flag.$$ToString(); + } + } + """); + } + + [Fact] + public async Task NoInlineDoSnippetWhenDottingBeforeContextualKeywordOnTheSameLineTest() + { + await VerifySnippetIsAbsentAsync(""" + class C + { + void M(bool flag) + { + flag.$$var a = 0; + } + } + """); + } + [Fact] public async Task NoInlineDoSnippetForTypeItselfTest() { diff --git a/src/Features/CSharpTest/Snippets/CSharpForEachSnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/CSharpForEachSnippetProviderTests.cs index ef08ca718abd8..2c92199bfcce0 100644 --- a/src/Features/CSharpTest/Snippets/CSharpForEachSnippetProviderTests.cs +++ b/src/Features/CSharpTest/Snippets/CSharpForEachSnippetProviderTests.cs @@ -598,6 +598,69 @@ void M(IEnumerable ints) """); } + [Fact] + public async Task InsertInlineForEachSnippetWhenDottingBeforeMemberAccessExpressionOnTheNextLineTest() + { + await VerifySnippetAsync(""" + using System; + + class C + { + void M(int[] ints) + { + ints.$$ + Console.WriteLine(); + } + } + """, """ + using System; + + class C + { + void M(int[] ints) + { + foreach (var {|0:item|} in ints) + { + $$ + } + Console.WriteLine(); + } + } + """); + } + + [Fact] + public async Task NoInlineForEachSnippetWhenDottingBeforeMemberAccessExpressionOnTheSameLineTest() + { + await VerifySnippetIsAbsentAsync(""" + using System; + + class C + { + void M(int[] ints) + { + ints.$$ToString(); + } + } + """); + } + + [Fact] + public async Task NoInlineForEachSnippetWhenDottingBeforeContextualKeywordOnTheSameLineTest() + { + await VerifySnippetIsAbsentAsync(""" + using System; + + class C + { + void M(int[] ints) + { + ints.$$var a = 0; + } + } + """); + } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/69598")] public async Task InsertInlineAwaitForEachSnippetWhenDottingBeforeContextualKeywordTest1() { @@ -701,6 +764,74 @@ void M(IAsyncEnumerable asyncInts) referenceAssemblies: ReferenceAssemblies.Net.Net70); } + [Fact] + public async Task InsertInlineAwaitForEachSnippetWhenDottingBeforeMemberAccessExpressionOnTheNextLineTest() + { + await VerifySnippetAsync(""" + using System; + using System.Collections.Generic; + + class C + { + void M(IAsyncEnumerable ints) + { + ints.$$ + Console.WriteLine(); + } + } + """, """ + using System; + using System.Collections.Generic; + + class C + { + void M(IAsyncEnumerable ints) + { + await foreach (var {|0:item|} in ints) + { + $$ + } + Console.WriteLine(); + } + } + """, + referenceAssemblies: ReferenceAssemblies.Net.Net80); + } + + [Fact] + public async Task NoInlineAwaitForEachSnippetWhenDottingBeforeMemberAccessExpressionOnTheSameLineTest() + { + await VerifySnippetIsAbsentAsync(""" + using System.Collections.Generic; + + class C + { + void M(IAsyncEnumerable ints) + { + ints.$$ToString(); + } + } + """, + referenceAssemblies: ReferenceAssemblies.Net.Net80); + } + + [Fact] + public async Task NoInlineAwaitForEachSnippetWhenDottingBeforeContextualKeywordOnTheSameLineTest() + { + await VerifySnippetIsAbsentAsync(""" + using System.Collections.Generic; + + class C + { + void M(IAsyncEnumerable ints) + { + ints.$$var a = 0; + } + } + """, + referenceAssemblies: ReferenceAssemblies.Net.Net80); + } + [Theory] [MemberData(nameof(CommonSnippetTestData.CommonEnumerableTypes), MemberType = typeof(CommonSnippetTestData))] public async Task NoInlineForEachSnippetForTypeItselfTest(string collectionType) diff --git a/src/Features/CSharpTest/Snippets/CSharpForSnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/CSharpForSnippetProviderTests.cs index c6535a98cb8d4..2363ffcb65836 100644 --- a/src/Features/CSharpTest/Snippets/CSharpForSnippetProviderTests.cs +++ b/src/Features/CSharpTest/Snippets/CSharpForSnippetProviderTests.cs @@ -568,6 +568,68 @@ void M(int @int) """); } + [Theory] + [MemberData(nameof(CommonSnippetTestData.IntegerTypes), MemberType = typeof(CommonSnippetTestData))] + public async Task InsertInlineForSnippetWhenDottingBeforeMemberAccessExpressionOnTheNextLineTest(string intType) + { + await VerifySnippetAsync($$""" + using System; + + class C + { + void M({{intType}} @int) + { + @int.$$ + Console.WriteLine(); + } + } + """, $$""" + using System; + + class C + { + void M({{intType}} @int) + { + for ({{intType}} {|0:i|} = 0; {|0:i|} < @int; {|0:i|}++) + { + $$ + } + Console.WriteLine(); + } + } + """); + } + + [Theory] + [MemberData(nameof(CommonSnippetTestData.IntegerTypes), MemberType = typeof(CommonSnippetTestData))] + public async Task NoInlineForSnippetWhenDottingBeforeMemberAccessExpressionOnTheSameLineTest(string intType) + { + await VerifySnippetIsAbsentAsync($$""" + class C + { + void M({{intType}} @int) + { + @int.$$ToString(); + } + } + """); + } + + [Theory] + [MemberData(nameof(CommonSnippetTestData.IntegerTypes), MemberType = typeof(CommonSnippetTestData))] + public async Task NoInlineForSnippetWhenDottingBeforeContextualKeywordOnTheSameLineTest(string intType) + { + await VerifySnippetIsAbsentAsync($$""" + class C + { + void M({{intType}} @int) + { + @int.$$var a = 0; + } + } + """); + } + [Theory] [InlineData("int[]", "Length")] [InlineData("Span", "Length")] diff --git a/src/Features/CSharpTest/Snippets/CSharpReversedForSnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/CSharpReversedForSnippetProviderTests.cs index 88a56ffefd939..c29a2e87b6e79 100644 --- a/src/Features/CSharpTest/Snippets/CSharpReversedForSnippetProviderTests.cs +++ b/src/Features/CSharpTest/Snippets/CSharpReversedForSnippetProviderTests.cs @@ -570,6 +570,68 @@ void M(int @int) """); } + [Theory] + [MemberData(nameof(CommonSnippetTestData.IntegerTypes), MemberType = typeof(CommonSnippetTestData))] + public async Task InsertInlineReversedForSnippetWhenDottingBeforeMemberAccessExpressionOnTheNextLineTest(string intType) + { + await VerifySnippetAsync($$""" + using System; + + class C + { + void M({{intType}} @int) + { + @int.$$ + Console.WriteLine(); + } + } + """, $$""" + using System; + + class C + { + void M({{intType}} @int) + { + for ({{intType}} {|0:i|} = @int - 1; {|0:i|} >= 0; {|0:i|}--) + { + $$ + } + Console.WriteLine(); + } + } + """); + } + + [Theory] + [MemberData(nameof(CommonSnippetTestData.IntegerTypes), MemberType = typeof(CommonSnippetTestData))] + public async Task NoInlineReversedForSnippetWhenDottingBeforeMemberAccessExpressionOnTheSameLineTest(string intType) + { + await VerifySnippetIsAbsentAsync($$""" + class C + { + void M({{intType}} @int) + { + @int.$$ToString(); + } + } + """); + } + + [Theory] + [MemberData(nameof(CommonSnippetTestData.IntegerTypes), MemberType = typeof(CommonSnippetTestData))] + public async Task NoInlineReversedForSnippetWhenDottingBeforeContextualKeywordOnTheSameLineTest(string intType) + { + await VerifySnippetIsAbsentAsync($$""" + class C + { + void M({{intType}} @int) + { + @int.$$var a = 0; + } + } + """); + } + [Theory] [InlineData("int[]", "Length")] [InlineData("Span", "Length")] diff --git a/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractInlineStatementSnippetProvider.cs b/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractInlineStatementSnippetProvider.cs index 4234f497d0d3a..a39f9f2d7d6c2 100644 --- a/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractInlineStatementSnippetProvider.cs +++ b/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractInlineStatementSnippetProvider.cs @@ -27,6 +27,8 @@ internal abstract class AbstractInlineStatementSnippetProvider /// Current compilation instance protected abstract bool IsValidAccessingType(ITypeSymbol type, Compilation compilation); + protected abstract bool CanInsertStatementAfterToken(SyntaxToken token); + /// /// Generate statement node /// @@ -75,14 +77,39 @@ protected sealed override async Task GenerateSnippetTextChangeAsync( return closestNode.FirstAncestorOrSelf(); } - private static bool TryGetInlineExpressionInfo(SyntaxToken targetToken, ISyntaxFactsService syntaxFacts, SemanticModel semanticModel, [NotNullWhen(true)] out InlineExpressionInfo? expressionInfo, CancellationToken cancellationToken) + private bool CanInsertStatementBeforeToken(SyntaxToken token) + { + var previousToken = token.GetPreviousToken(); + if (previousToken == default) + { + // Token is the first token in the file + return true; + } + + return CanInsertStatementAfterToken(previousToken); + } + + private bool TryGetInlineExpressionInfo( + SyntaxToken targetToken, + ISyntaxFactsService syntaxFacts, + SemanticModel semanticModel, + [NotNullWhen(true)] out InlineExpressionInfo? expressionInfo, + CancellationToken cancellationToken) { var parentNode = targetToken.Parent; if (syntaxFacts.IsMemberAccessExpression(parentNode) && - syntaxFacts.IsExpressionStatement(parentNode?.Parent)) + CanInsertStatementBeforeToken(parentNode.GetFirstToken())) { - var expression = syntaxFacts.GetExpressionOfMemberAccessExpression(parentNode)!; + syntaxFacts.GetPartsOfMemberAccessExpression(parentNode, out var expression, out var dotToken, out var name); + var sourceText = parentNode.SyntaxTree.GetText(cancellationToken); + + if (sourceText.AreOnSameLine(dotToken, name.GetFirstToken())) + { + expressionInfo = null; + return false; + } + var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken); // Forbid a case when we are dotting of a type, e.g. `string.$$`. @@ -105,9 +132,17 @@ private static bool TryGetInlineExpressionInfo(SyntaxToken targetToken, ISyntaxF // var a = 0; // ... // Here `flag.var` is parsed as a qualified name, so this case requires its own handling - if (syntaxFacts.IsQualifiedName(parentNode)) + if (syntaxFacts.IsQualifiedName(parentNode) && CanInsertStatementBeforeToken(parentNode.GetFirstToken())) { - syntaxFacts.GetPartsOfQualifiedName(parentNode, out var expression, out _, out _); + syntaxFacts.GetPartsOfQualifiedName(parentNode, out var expression, out var dotToken, out var right); + var sourceText = parentNode.SyntaxTree.GetText(cancellationToken); + + if (sourceText.AreOnSameLine(dotToken, right.GetFirstToken())) + { + expressionInfo = null; + return false; + } + var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken); // Forbid a case when we are dotting of a type, e.g. `string.$$`.