// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; using Moq; using Xunit; namespace Microsoft.VisualStudio.Editor.Razor { public class RazorDirectiveCompletionProviderTest { private static readonly IReadOnlyList DefaultDirectives = new[] { CSharpCodeParser.AddTagHelperDirectiveDescriptor, CSharpCodeParser.RemoveTagHelperDirectiveDescriptor, CSharpCodeParser.TagHelperPrefixDirectiveDescriptor, }; [Fact] public async Task GetDescriptionAsync_AddsDirectiveDescriptionIfPropertyExists() { // Arrange var document = CreateDocument(); var expectedDescription = "The expected description"; var item = CompletionItem.Create("TestDirective") .WithProperties((new Dictionary() { [RazorDirectiveCompletionProvider.DescriptionKey] = expectedDescription, }).ToImmutableDictionary()); var codeDocumentProvider = new Mock(); var completionProvider = new RazorDirectiveCompletionProvider(new Lazy(() => codeDocumentProvider.Object)); // Act var description = await completionProvider.GetDescriptionAsync(document, item, CancellationToken.None); // Assert var part = Assert.Single(description.TaggedParts); Assert.Equal(TextTags.Text, part.Tag); Assert.Equal(expectedDescription, part.Text); Assert.Equal(expectedDescription, description.Text); } [Fact] public async Task GetDescriptionAsync_DoesNotAddDescriptionWhenPropertyAbsent() { // Arrange var document = CreateDocument(); var item = CompletionItem.Create("TestDirective"); var codeDocumentProvider = new Mock(); var completionProvider = new RazorDirectiveCompletionProvider(new Lazy(() => codeDocumentProvider.Object)); // Act var description = await completionProvider.GetDescriptionAsync(document, item, CancellationToken.None); // Assert Assert.Empty(description.TaggedParts); Assert.Equal(string.Empty, description.Text); } [Fact] public async Task ProvideCompletionAsync_DoesNotProvideCompletionsForNonRazorFiles() { // Arrange var codeDocumentProvider = new Mock(MockBehavior.Strict); var completionProvider = new FailOnGetCompletionsProvider(new Lazy(() => codeDocumentProvider.Object)); var document = CreateDocument(); document = document.WithFilePath("NotRazor.cs"); var context = CreateContext(1, completionProvider, document); // Act & Assert await completionProvider.ProvideCompletionsAsync(context); } [Fact] public async Task ProvideCompletionAsync_DoesNotProvideCompletionsForDocumentWithoutPath() { // Arrange var project = ProjectInfo .Create(ProjectId.CreateNewId(), VersionStamp.Default, "TestProject", "TestAssembly", LanguageNames.CSharp) .WithFilePath("/TestProject.csproj"); var workspace = new AdhocWorkspace(); workspace.AddProject(project); var documentInfo = DocumentInfo.Create(DocumentId.CreateNewId(project.Id), "Test.cshtml"); var document = workspace.AddDocument(documentInfo); var codeDocumentProvider = new Mock(MockBehavior.Strict); var completionProvider = new FailOnGetCompletionsProvider(new Lazy(() => codeDocumentProvider.Object)); var context = CreateContext(1, completionProvider, document); // Act & Assert await completionProvider.ProvideCompletionsAsync(context); } [Fact] public async Task ProvideCompletionAsync_DoesNotProvideCompletionsWhenDocumentProviderCanNotGetDocument() { // Arrange RazorCodeDocument codeDocument; var codeDocumentProvider = new Mock(); codeDocumentProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny(), out codeDocument)) .Returns(false); var completionProvider = new FailOnGetCompletionsProvider(new Lazy(() => codeDocumentProvider.Object)); var document = CreateDocument(); var context = CreateContext(1, completionProvider, document); // Act & Assert await completionProvider.ProvideCompletionsAsync(context); } [Fact] public async Task ProvideCompletionAsync_DoesNotProvideCompletionsCanNotFindSnapshotPoint() { // Arrange var codeDocumentProvider = CreateCodeDocumentProvider("@", Enumerable.Empty()); var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider, false); var document = CreateDocument(); var context = CreateContext(0, completionProvider, document); // Act & Assert await completionProvider.ProvideCompletionsAsync(context); } [Fact] public async Task ProvideCompletionAsync_DoesNotProvideCompletionsWhenNotAtCompletionPoint() { // Arrange var codeDocumentProvider = CreateCodeDocumentProvider("@", Enumerable.Empty()); var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider); var document = CreateDocument(); var context = CreateContext(0, completionProvider, document); // Act & Assert await completionProvider.ProvideCompletionsAsync(context); } [Theory] [InlineData("DateTime.Now")] [InlineData("SomeMethod()")] public async Task ProvideCompletionAsync_DoesNotProvideCompletionsWhenAtComplexExpressions(string content) { // Arrange var codeDocumentProvider = CreateCodeDocumentProvider("@" + content, Enumerable.Empty()); var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider); var document = CreateDocument(); var context = CreateContext(1, completionProvider, document); // Act & Assert await completionProvider.ProvideCompletionsAsync(context); } [Fact] public async Task ProvideCompletionAsync_DoesNotProvideCompletionsForExplicitExpressions() { // Arrange var codeDocumentProvider = CreateCodeDocumentProvider("@()", Enumerable.Empty()); var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider); var document = CreateDocument(); var context = CreateContext(2, completionProvider, document); // Act & Assert await completionProvider.ProvideCompletionsAsync(context); } [Fact] public async Task ProvideCompletionAsync_DoesNotProvideCompletionsForCodeDocumentWithoutSyntaxTree() { // Arrange var codeDocumentProvider = new Mock(); var codeDocument = TestRazorCodeDocument.CreateEmpty(); codeDocumentProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny(), out codeDocument)) .Returns(true); var completionProvider = new FailOnGetCompletionsProvider(new Lazy(() => codeDocumentProvider.Object)); var document = CreateDocument(); var context = CreateContext(2, completionProvider, document); // Act & Assert await completionProvider.ProvideCompletionsAsync(context); } [Fact] public void GetCompletionItems_ProvidesCompletionsForDefaultDirectives() { // Arrange var codeDocumentProvider = CreateCodeDocumentProvider("@", Enumerable.Empty()); var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider); var document = CreateDocument(); codeDocumentProvider.Value.TryGetFromDocument(document, out var codeDocument); var syntaxTree = codeDocument.GetSyntaxTree(); // Act var completionItems = completionProvider.GetCompletionItems(syntaxTree); // Assert Assert.Collection( completionItems, item => AssertRazorCompletionItem(DefaultDirectives[0].Description, item), item => AssertRazorCompletionItem(DefaultDirectives[1].Description, item), item => AssertRazorCompletionItem(DefaultDirectives[2].Description, item)); } [Fact] public void GetCompletionItems_ProvidesCompletionsForDefaultAndExtensibleDirectives() { // Arrange var codeDocumentProvider = CreateCodeDocumentProvider("@", new[] { SectionDirective.Directive }); var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider); var document = CreateDocument(); codeDocumentProvider.Value.TryGetFromDocument(document, out var codeDocument); var syntaxTree = codeDocument.GetSyntaxTree(); // Act var completionItems = completionProvider.GetCompletionItems(syntaxTree); // Assert Assert.Collection( completionItems, item => AssertRazorCompletionItem(SectionDirective.Directive.Description, item), item => AssertRazorCompletionItem(DefaultDirectives[0].Description, item), item => AssertRazorCompletionItem(DefaultDirectives[1].Description, item), item => AssertRazorCompletionItem(DefaultDirectives[2].Description, item)); } [Fact] public void GetCompletionItems_ProvidesCompletionsForDirectivesWithoutDescription() { // Arrange var customDirective = DirectiveDescriptor.CreateSingleLineDirective("custom"); var codeDocumentProvider = CreateCodeDocumentProvider("@", new[] { customDirective }); var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider); var document = CreateDocument(); codeDocumentProvider.Value.TryGetFromDocument(document, out var codeDocument); var syntaxTree = codeDocument.GetSyntaxTree(); // Act var completionItems = completionProvider.GetCompletionItems(syntaxTree); // Assert var customDirectiveCompletion = Assert.Single(completionItems, item => item.DisplayText == customDirective.Directive); AssertRazorCompletionItemDefaults(customDirectiveCompletion); Assert.DoesNotContain(customDirectiveCompletion.Properties, property => property.Key == RazorDirectiveCompletionProvider.DescriptionKey); } private static void AssertRazorCompletionItem(string expectedDescription, CompletionItem item) { Assert.True(item.Properties.TryGetValue(RazorDirectiveCompletionProvider.DescriptionKey, out var actualDescription)); Assert.Equal(expectedDescription, actualDescription); AssertRazorCompletionItemDefaults(item); } private static void AssertRazorCompletionItemDefaults(CompletionItem item) { Assert.Equal("_RazorDirective_", item.SortText); Assert.False(item.Rules.FormatOnCommit); var tag = Assert.Single(item.Tags); Assert.Equal(CompletionTags.Intrinsic, tag); } private static Lazy CreateCodeDocumentProvider(string text, IEnumerable directives) { var codeDocumentProvider = new Mock(); var codeDocument = TestRazorCodeDocument.CreateEmpty(); var sourceDocument = TestRazorSourceDocument.Create(text); var options = RazorParserOptions.Create(builder => { foreach (var directive in directives) { builder.Directives.Add(directive); } }); var syntaxTree = RazorSyntaxTree.Parse(sourceDocument, options); codeDocument.SetSyntaxTree(syntaxTree); codeDocumentProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny(), out codeDocument)) .Returns(true); return new Lazy(() => codeDocumentProvider.Object); } private static CompletionContext CreateContext(int position, RazorDirectiveCompletionProvider completionProvider, Document document) { var context = new CompletionContext( completionProvider, document, position, TextSpan.FromBounds(position, position), CompletionTrigger.Invoke, new Mock().Object, CancellationToken.None); return context; } private static Document CreateDocument() { var project = ProjectInfo .Create(ProjectId.CreateNewId(), VersionStamp.Default, "TestProject", "TestAssembly", LanguageNames.CSharp) .WithFilePath("/TestProject.csproj"); var workspace = new AdhocWorkspace(); workspace.AddProject(project); var documentInfo = DocumentInfo.Create(DocumentId.CreateNewId(project.Id), "Test.cshtml"); var document = workspace.AddDocument(documentInfo); document = document.WithFilePath("Test.cshtml"); return document; } private class FailOnGetCompletionsProvider : RazorDirectiveCompletionProvider { private readonly bool _canGetSnapshotPoint; public FailOnGetCompletionsProvider(Lazy codeDocumentProvider, bool canGetSnapshotPoint = true) : base(codeDocumentProvider) { _canGetSnapshotPoint = canGetSnapshotPoint; } internal override IEnumerable GetCompletionItems(RazorSyntaxTree syntaxTree) { Assert.False(true, "Completions should not have been attempted."); return null; } protected override bool TryGetRazorSnapshotPoint(CompletionContext context, out SnapshotPoint snapshotPoint) { if (!_canGetSnapshotPoint) { snapshotPoint = default(SnapshotPoint); return false; } var snapshot = new Mock(MockBehavior.Strict); snapshot.Setup(s => s.Length) .Returns(context.CompletionListSpan.End); snapshotPoint = new SnapshotPoint(snapshot.Object, context.CompletionListSpan.Start); return true; } } } }