diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs index 6df55c5e6ee55..a7b7a5ce93764 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs @@ -51,6 +51,7 @@ internal sealed class SnippetCompletionProvider : LSPCompletionProvider CSharpSnippetIdentifiers.StaticIntMain, CSharpSnippetIdentifiers.Struct, CSharpSnippetIdentifiers.StaticVoidMain, + CSharpSnippetIdentifiers.Using, CSharpSnippetIdentifiers.While ]; diff --git a/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs b/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs index 406a380e2ca8a..0631779f5892a 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs @@ -25,5 +25,6 @@ internal static class CSharpSnippetIdentifiers public const string StaticIntMain = "sim"; public const string Struct = "struct"; public const string StaticVoidMain = "svm"; + public const string Using = "using"; public const string While = "while"; } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpUsingSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpUsingSnippetProvider.cs new file mode 100644 index 0000000000000..65f148c6f587e --- /dev/null +++ b/src/Features/CSharp/Portable/Snippets/CSharpUsingSnippetProvider.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageService; +using Microsoft.CodeAnalysis.Snippets; +using Microsoft.CodeAnalysis.Snippets.SnippetProviders; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpUsingSnippetProvider() : AbstractUsingSnippetProvider +{ + public override string Identifier => CSharpSnippetIdentifiers.Using; + + public override string Description => CSharpFeaturesResources.using_statement; + + protected override ImmutableArray GetPlaceHolderLocationsList(UsingStatementSyntax node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) + { + var expression = node.Expression!; + return [new SnippetPlaceholder(expression.ToString(), expression.SpanStart)]; + } + + protected override int GetTargetCaretPosition(UsingStatementSyntax usingStatement, SourceText sourceText) + => CSharpSnippetHelpers.GetTargetCaretPositionInBlock( + usingStatement, + static s => (BlockSyntax)s.Statement, + sourceText); + + protected override Task AddIndentationToDocumentAsync(Document document, UsingStatementSyntax usingStatement, CancellationToken cancellationToken) + => CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( + document, + usingStatement, + static s => (BlockSyntax)s.Statement, + cancellationToken); +} diff --git a/src/Features/CSharpTest/Snippets/CSharpUsingSnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/CSharpUsingSnippetProviderTests.cs new file mode 100644 index 0000000000000..3cc449347bab4 --- /dev/null +++ b/src/Features/CSharpTest/Snippets/CSharpUsingSnippetProviderTests.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Snippets; + +[Trait(Traits.Feature, Traits.Features.Snippets)] +public sealed class CSharpUsingSnippetProviderTests : AbstractCSharpSnippetProviderTests +{ + protected override string SnippetIdentifier => "using"; + + [Fact] + public async Task InsertUsingSnippetInMethodTest() + { + await VerifySnippetAsync(""" + class Program + { + public void Method() + { + $$ + } + } + """, """ + class Program + { + public void Method() + { + using ({|0:resource|}) + { + $$ + } + } + } + """); + } + + [Fact] + public async Task InsertUsingSnippetInGlobalContextTest() + { + await VerifySnippetAsync(""" + $$ + """, """ + using ({|0:resource|}) + { + $$ + } + """); + } + + [Fact] + public async Task NoUsingSnippetInBusingNamespaceTest() + { + await VerifySnippetIsAbsentAsync(""" + namespace Namespace + { + $$ + } + """); + } + + [Fact] + public async Task NoUsingSnippetInFileScopedNamespaceTest() + { + await VerifySnippetIsAbsentAsync(""" + namespace Namespace; + $$ + """); + } + + [Fact] + public async Task InsertUsingSnippetInConstructorTest() + { + await VerifySnippetAsync(""" + class Program + { + public Program() + { + $$ + } + } + """, """ + class Program + { + public Program() + { + using ({|0:resource|}) + { + $$ + } + } + } + """); + } + + [Fact] + public async Task NoUsingSnippetInTypeBodyTest() + { + await VerifySnippetIsAbsentAsync(""" + class Program + { + $$ + } + """); + } + + [Fact] + public async Task InsertUsingSnippetInLocalFunctionTest() + { + await VerifySnippetAsync(""" + class Program + { + public void Method() + { + void LocalFunction() + { + $$ + } + } + } + """, """ + class Program + { + public void Method() + { + void LocalFunction() + { + using ({|0:resource|}) + { + $$ + } + } + } + } + """); + } + + [Fact] + public async Task InsertUsingSnippetInAnonymousFunctionTest() + { + await VerifySnippetAsync(""" + class Program + { + public void Method() + { + var action = delegate() + { + $$ + }; + } + } + """, """ + class Program + { + public void Method() + { + var action = delegate() + { + using ({|0:resource|}) + { + $$ + } + }; + } + } + """); + } + + [Fact] + public async Task InsertUsingSnippetInParenthesizedLambdaExpressionTest() + { + await VerifySnippetAsync(""" + class Program + { + public void Method() + { + var action = () => + { + $$ + }; + } + } + """, """ + class Program + { + public void Method() + { + var action = () => + { + using ({|0:resource|}) + { + $$ + } + }; + } + } + """); + } +} diff --git a/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractUsingSnippetProvider.cs b/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractUsingSnippetProvider.cs new file mode 100644 index 0000000000000..f64d64c1af24b --- /dev/null +++ b/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractUsingSnippetProvider.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Snippets.SnippetProviders; + +internal abstract class AbstractUsingSnippetProvider : AbstractStatementSnippetProvider + where TUsingStatementSyntax : SyntaxNode +{ + protected sealed override async Task GenerateSnippetTextChangeAsync(Document document, int position, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var identifierName = NameGenerator.GenerateUniqueName("resource", + n => semanticModel.LookupSymbols(position, name: n).IsEmpty); + var statement = generator.UsingStatement(generator.IdentifierName(identifierName), statements: []); + return new TextChange(TextSpan.FromBounds(position, position), statement.NormalizeWhitespace().ToFullString()); + } +}