Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Semantic Snippets - struct + a lot of abstracting #64016

Merged
merged 2 commits into from
Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// 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.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Completion.CompletionProviders.Snippets
{
[Trait(Traits.Feature, Traits.Features.Completion)]
public class CSharpStructSnippetCompletionProviderTests : AbstractCSharpSnippetCompletionProviderTests
{
protected override string ItemToCommit => "struct";

[WpfFact]
public async Task InsertStructSnippetInNamespaceTest()
{
var markupBeforeCommit =
@"namespace Namespace
{
$$
}";

var expectedCodeAfterCommit =
@"namespace Namespace
{
struct MyStruct
{
$$
}
}";
await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit);
}

[WpfFact]
public async Task InsertStructSnippetInFileScopedNamespaceTest()
{
var markupBeforeCommit =
@"namespace Namespace;

$$";

var expectedCodeAfterCommit =
@"namespace Namespace;

struct MyStruct
{
$$
}"
;
await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit);
}

[WpfFact]
public async Task InsertStructSnippetTest()
{
var markupBeforeCommit =
@"$$";

var expectedCodeAfterCommit =
@"struct MyStruct
{
$$
}"
;
await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit);
}

[WpfFact]
public async Task InsertStructTopLevelSnippetTest()
{
var markupBeforeCommit =
@"System.Console.WriteLine();
$$";

var expectedCodeAfterCommit =
@"System.Console.WriteLine();

struct MyStruct
{
$$
}"
;
await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit);
}

[WpfFact]
public async Task InsertStructSnippetInClassTest()
{
var markupBeforeCommit =
@"struct MyClass
{
$$
}"
;

var expectedCodeAfterCommit =
@"struct MyClass
{
struct MyStruct
{
$$
}
}"
;
await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit);
}

[WpfFact]
public async Task InsertStructSnippetInRecordTest()
{
var markupBeforeCommit =
@"record MyRecord
{
$$
}";

var expectedCodeAfterCommit =
@"record MyRecord
{
struct MyStruct
{
$$
}
}"
;
await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit);
}

[WpfFact]
public async Task InsertStructSnippetInStructTest()
{
var markupBeforeCommit =
@"struct MyStruct
{
$$
}";

var expectedCodeAfterCommit =
@"struct MyStruct
{
struct MyStruct1
{
$$
}
}"
;
await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit);
}

[WpfFact]
public async Task InsertStructSnippetInInterfaceTest()
{
var markupBeforeCommit =
@"interface MyInterface
{
$$
}";

var expectedCodeAfterCommit =
@"interface MyInterface
{
struct MyStruct
{
$$
}
}"
;
await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit);
}

[WpfFact]
public async Task InsertStructSnippetWithModifiersTest()
{
var markupBeforeCommit =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" CommonReferences=""true"">
<Document FilePath=""/0/Test0.cs"">
$$
</Document>
<AnalyzerConfigDocument FilePath=""/.editorconfig"">
root = true

[*]
# IDE0008: Use explicit type
dotnet_style_require_accessibility_modifiers = always
</AnalyzerConfigDocument>
</Project>
</Workspace>";
var expectedCodeAfterCommit =
$@"
public struct MyStruct
{{
$$
}}
";

await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit);
}

[WpfFact]
public async Task NoStructSnippetInEnumTest()
{
var markupBeforeCommit =
@"enum MyEnum
{
$$
}";

await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit);
}

[WpfFact]
public async Task NoStructSnippetInMethodTest()
{
var markupBeforeCommit =
@"struct Program
{
public void Method()
{
$$
}
}"
;
await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit);
}

[WpfFact]
public async Task NoStructSnippetInConstructorTest()
{
var markupBeforeCommit =
@"struct Program
{
public Program()
{
$$
}
}"
;
await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,116 +3,47 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Snippets;
using Microsoft.CodeAnalysis.Snippets.SnippetProviders;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Snippets
{
[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared]
internal class CSharpClassSnippetProvider : AbstractClassSnippetProvider
internal class CSharpClassSnippetProvider : CSharpTypeSnippetProvider
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CSharpClassSnippetProvider()
{
}
public override string SnippetIdentifier => "class";

private static readonly ISet<SyntaxKind> s_validModifiers = new HashSet<SyntaxKind>(SyntaxFacts.EqualityComparer)
{
SyntaxKind.NewKeyword,
SyntaxKind.PublicKeyword,
SyntaxKind.ProtectedKeyword,
SyntaxKind.InternalKeyword,
SyntaxKind.PrivateKeyword,
SyntaxKind.AbstractKeyword,
SyntaxKind.SealedKeyword,
SyntaxKind.StaticKeyword,
SyntaxKind.UnsafeKeyword
};
public override string SnippetDescription => FeaturesResources.class_;

protected override async Task<bool> IsValidSnippetLocationAsync(Document document, int position, CancellationToken cancellationToken)
protected override async Task<SyntaxNode> GenerateTypeDeclarationAsync(Document document, int position, bool useAccessibility, CancellationToken cancellationToken)
{
var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false);
var syntaxContext = (CSharpSyntaxContext)document.GetRequiredLanguageService<ISyntaxContextService>().CreateContext(document, semanticModel, position, cancellationToken);
var generator = SyntaxGenerator.GetGenerator(document);
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

return
syntaxContext.IsGlobalStatementContext ||
syntaxContext.IsTypeDeclarationContext(
validModifiers: s_validModifiers,
validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations,
canBePartial: true,
cancellationToken: cancellationToken);
}

protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget, SourceText sourceText)
{
var classDeclaration = (ClassDeclarationSyntax)caretTarget;
var triviaSpan = classDeclaration.CloseBraceToken.LeadingTrivia.Span;
var line = sourceText.Lines.GetLineFromPosition(triviaSpan.Start);
// Getting the location at the end of the line before the newline.
return line.Span.End;
}

private static string GetIndentation(Document document, SyntaxNode node, SyntaxFormattingOptions syntaxFormattingOptions, CancellationToken cancellationToken)
{
var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken);
var classDeclarationSyntax = (ClassDeclarationSyntax)node;
var openBraceLine = parsedDocument.Text.Lines.GetLineFromPosition(classDeclarationSyntax.OpenBraceToken.SpanStart).LineNumber;

var indentationOptions = new IndentationOptions(syntaxFormattingOptions);
var newLine = indentationOptions.FormattingOptions.NewLine;

var indentationService = parsedDocument.LanguageServices.GetRequiredService<IIndentationService>();
var indentation = indentationService.GetIndentation(parsedDocument, openBraceLine + 1, indentationOptions, cancellationToken);

// Adding the offset calculated with one tab so that it is indented once past the line containing the opening brace
var newIndentation = new IndentationResult(indentation.BasePosition, indentation.Offset + syntaxFormattingOptions.TabSize);
return newIndentation.GetIndentationString(parsedDocument.Text, syntaxFormattingOptions.UseTabs, syntaxFormattingOptions.TabSize) + newLine;
}

protected override async Task<Document> AddIndentationToDocumentAsync(Document document, int position, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken)
{
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var snippet = root.GetAnnotatedNodes(_findSnippetAnnotation).FirstOrDefault();

var syntaxFormattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions: null, cancellationToken).ConfigureAwait(false);
var indentationString = GetIndentation(document, snippet, syntaxFormattingOptions, cancellationToken);

var originalClassDeclaration = (ClassDeclarationSyntax)snippet;
var newClassDeclaration = originalClassDeclaration.WithCloseBraceToken(
originalClassDeclaration.CloseBraceToken.WithPrependedLeadingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, indentationString)));
var name = NameGenerator.GenerateUniqueName("MyClass", name => semanticModel.LookupSymbols(position, name: name).IsEmpty);
var classDeclaration = useAccessibility is true
? generator.ClassDeclaration(name, accessibility: Accessibility.Public)
: generator.ClassDeclaration(name);

var newRoot = root.ReplaceNode(originalClassDeclaration, newClassDeclaration.WithAdditionalAnnotations(_cursorAnnotation, _findSnippetAnnotation));
return document.WithSyntaxRoot(newRoot);
return classDeclaration;
}

protected override void GetClassDeclarationIdentifier(SyntaxNode node, out SyntaxToken identifier)
protected override Func<SyntaxNode?, bool> GetSnippetContainerFunction(ISyntaxFacts syntaxFacts)
{
var classDeclarationSyntax = (ClassDeclarationSyntax)node;
identifier = classDeclarationSyntax.Identifier;
return syntaxFacts.IsClassDeclaration;
}
}
}
Loading