From 1795fc26c124d643fa24bcf0111c7112d0a84a92 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Fri, 28 Apr 2017 14:45:07 -0700 Subject: [PATCH] Add AttributeCompletion API. - Added a `GetAttributeCompletions` API that's consistent with current Razor's editor expectations. - Added unit tests to validate all code paths of the new `GetAttributeCompletions` method. #1120 --- .../AttributeCompletionContext.cs | 65 +++ .../AttributeCompletionResult.cs | 41 ++ .../DefaultTagHelperCompletionService.cs | 104 ++++ .../TagHelperCompletionService.cs | 2 + .../DefaultTagHelperCompletionServiceTest.cs | 455 +++++++++++++++++- 5 files changed, 652 insertions(+), 15 deletions(-) create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionContext.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionResult.cs diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionContext.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionContext.cs new file mode 100644 index 0000000000..bf815aa8c0 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionContext.cs @@ -0,0 +1,65 @@ +// 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 Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + public class AttributeCompletionContext + { + public AttributeCompletionContext( + TagHelperDocumentContext documentContext, + IEnumerable existingCompletions, + string currentTagName, + IEnumerable> attributes, + string currentParentTagName, + Func inHTMLSchema) + { + if (documentContext == null) + { + throw new ArgumentNullException(nameof(documentContext)); + } + + if (existingCompletions == null) + { + throw new ArgumentNullException(nameof(existingCompletions)); + } + + if (currentTagName == null) + { + throw new ArgumentNullException(nameof(currentTagName)); + } + + if (attributes == null) + { + throw new ArgumentNullException(nameof(attributes)); + } + + if (inHTMLSchema == null) + { + throw new ArgumentNullException(nameof(inHTMLSchema)); + } + + DocumentContext = documentContext; + ExistingCompletions = existingCompletions; + CurrentTagName = currentTagName; + Attributes = attributes; + CurrentParentTagName = currentParentTagName; + InHTMLSchema = inHTMLSchema; + } + + public TagHelperDocumentContext DocumentContext { get; } + + public IEnumerable ExistingCompletions { get; } + + public string CurrentTagName { get; } + + public IEnumerable> Attributes { get; } + + public string CurrentParentTagName { get; } + + public Func InHTMLSchema { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionResult.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionResult.cs new file mode 100644 index 0000000000..b5880d4c7f --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionResult.cs @@ -0,0 +1,41 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + public abstract class AttributeCompletionResult + { + private AttributeCompletionResult() + { + } + + public abstract IReadOnlyDictionary> Completions { get; } + + internal static AttributeCompletionResult Create(Dictionary> completions) + { + var readonlyCompletions = completions.ToDictionary( + key => key.Key, + value => (IEnumerable)value.Value, + completions.Comparer); + var result = new DefaultAttributeCompletionResult(readonlyCompletions); + + return result; + } + + private class DefaultAttributeCompletionResult : AttributeCompletionResult + { + private readonly IReadOnlyDictionary> _completions; + + public DefaultAttributeCompletionResult(IReadOnlyDictionary> completions) + { + _completions = completions; + } + + public override IReadOnlyDictionary> Completions => _completions; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperCompletionService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperCompletionService.cs index bf81b9161f..cf9a396e59 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperCompletionService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperCompletionService.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; +using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Razor.Language; @@ -21,6 +22,109 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor _tagHelperFactsService = tagHelperFactsService; } + /* + * This API attempts to understand a users context as they're typing in a Razor file to provide TagHelper based attribute IntelliSense. + * + * Scenarios for TagHelper attribute IntelliSense follows: + * 1. TagHelperDescriptor's have matching required attribute names + * -> Provide IntelliSense for the required attributes of those descriptors to lead users towards a TagHelperified element. + * 2. TagHelperDescriptor entirely applies to current element. Tag name, attributes, everything is fulfilled. + * -> Provide IntelliSense for the bound attributes for the applied descriptors. + * + * Within each of the above scenarios if an attribute completion has a corresponding bound attribute we associate it with the corresponding + * BoundAttributeDescriptor. By doing this a user can see what C# type a TagHelper expects for the attribute. + */ + public override AttributeCompletionResult GetAttributeCompletions(AttributeCompletionContext completionContext) + { + if (completionContext == null) + { + throw new ArgumentNullException(nameof(completionContext)); + } + + var attributeCompletions = completionContext.ExistingCompletions.ToDictionary( + completion => completion, + _ => new HashSet(), + StringComparer.OrdinalIgnoreCase); + + var documentContext = completionContext.DocumentContext; + var descriptorsForTag = _tagHelperFactsService.GetTagHelpersGivenTag(documentContext, completionContext.CurrentTagName, completionContext.CurrentParentTagName); + if (descriptorsForTag.Count == 0) + { + // If the current tag has no possible descriptors then we can't have any additional attributes. + var defaultResult = AttributeCompletionResult.Create(attributeCompletions); + return defaultResult; + } + + var prefix = documentContext.Prefix ?? string.Empty; + Debug.Assert(completionContext.CurrentTagName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + + var applicableTagHelperBinding = _tagHelperFactsService.GetTagHelperBinding(documentContext, completionContext.CurrentTagName, completionContext.Attributes, completionContext.CurrentParentTagName); + var applicableDescriptors = applicableTagHelperBinding?.Descriptors ?? Enumerable.Empty(); + var unprefixedTagName = completionContext.CurrentTagName.Substring(prefix.Length); + + if (!completionContext.InHTMLSchema(unprefixedTagName) && + applicableDescriptors.All(descriptor => descriptor.TagOutputHint == null)) + { + // This isn't a known HTML tag and no descriptor has an output element hint. Remove all previous completions. + attributeCompletions.Clear(); + } + + for (var i = 0; i < descriptorsForTag.Count; i++) + { + var descriptor = descriptorsForTag[i]; + + if (applicableDescriptors.Contains(descriptor)) + { + foreach (var attributeDescriptor in descriptor.BoundAttributes) + { + UpdateCompletions(attributeDescriptor.Name, attributeDescriptor); + } + } + else + { + var htmlNameToBoundAttribute = descriptor.BoundAttributes.ToDictionary(attribute => attribute.Name, StringComparer.OrdinalIgnoreCase); + + foreach (var rule in descriptor.TagMatchingRules) + { + foreach (var requiredAttribute in rule.Attributes) + { + if (htmlNameToBoundAttribute.TryGetValue(requiredAttribute.Name, out var attributeDescriptor)) + { + UpdateCompletions(requiredAttribute.Name, attributeDescriptor); + } + else + { + UpdateCompletions(requiredAttribute.Name, possibleDescriptor: null); + } + } + } + } + } + + var completionResult = AttributeCompletionResult.Create(attributeCompletions); + return completionResult; + + void UpdateCompletions(string attributeName, BoundAttributeDescriptor possibleDescriptor) + { + if (completionContext.Attributes.Any(attribute => string.Equals(attribute.Key, attributeName, StringComparison.OrdinalIgnoreCase))) + { + // Attribute is already present on this element it shouldn't exist in the completion list. + return; + } + + if (!attributeCompletions.TryGetValue(attributeName, out var rules)) + { + rules = new HashSet(); + attributeCompletions[attributeName] = rules; + } + + if (possibleDescriptor != null) + { + rules.Add(possibleDescriptor); + } + } + } + public override ElementCompletionResult GetElementCompletions(ElementCompletionContext completionContext) { if (completionContext == null) diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperCompletionService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperCompletionService.cs index a2673f705c..f6412dc2c0 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperCompletionService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperCompletionService.cs @@ -5,6 +5,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor { public abstract class TagHelperCompletionService { + public abstract AttributeCompletionResult GetAttributeCompletions(AttributeCompletionContext completionContext); + public abstract ElementCompletionResult GetElementCompletions(ElementCompletionContext completionContext); } } diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperCompletionServiceTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperCompletionServiceTest.cs index c16b9eca9f..70f98ed3fc 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperCompletionServiceTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperCompletionServiceTest.cs @@ -10,6 +10,399 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor { public class DefaultTagHelperCompletionServiceTest { + [Fact] + public void GetAttributeCompletions_DoesNotReturnCompletionsForAlreadySuppliedAttributes() + { + // Arrange + var documentDescriptors = new[] + { + TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule + .RequireTagName("div") + .RequireAttribute(attribute => attribute.Name("repeat"))) + .BindAttribute(attribute => attribute + .Name("visible") + .TypeName(typeof(bool).FullName) + .PropertyName("Visible")) + .Build(), + TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule.RequireTagName("*")) + .BindAttribute(attribute => attribute + .Name("class") + .TypeName(typeof(string).FullName) + .PropertyName("Class")) + .Build(), + }; + var expectedCompletions = AttributeCompletionResult.Create(new Dictionary>() + { + ["onclick"] = new HashSet(), + ["visible"] = new HashSet() + { + documentDescriptors[0].BoundAttributes.Last() + } + }); + + var existingCompletions = new[] { "onclick" }; + var completionContext = BuildAttributeCompletionContext( + documentDescriptors, + existingCompletions, + attributes: new Dictionary() + { + ["class"] = "something", + ["repeat"] = "4" + }, + currentTagName: "div"); + var service = CreateTagHelperCompletionFactsService(); + + // Act + var completions = service.GetAttributeCompletions(completionContext); + + // Assert + AssertCompletionsAreEquivalent(expectedCompletions, completions); + } + + [Fact] + public void GetAttributeCompletions_PossibleDescriptorsReturnUnboundRequiredAttributesWithExistingCompletions() + { + // Arrange + var documentDescriptors = new[] + { + TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule + .RequireTagName("div") + .RequireAttribute(attribute => attribute.Name("repeat"))) + .Build(), + TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule + .RequireTagName("*") + .RequireAttribute(attribute => attribute.Name("class"))) + .Build(), + }; + var expectedCompletions = AttributeCompletionResult.Create(new Dictionary>() + { + ["class"] = new HashSet(), + ["onclick"] = new HashSet(), + ["repeat"] = new HashSet() + }); + + var existingCompletions = new[] { "onclick", "class" }; + var completionContext = BuildAttributeCompletionContext( + documentDescriptors, + existingCompletions, + currentTagName: "div"); + var service = CreateTagHelperCompletionFactsService(); + + // Act + var completions = service.GetAttributeCompletions(completionContext); + + // Assert + AssertCompletionsAreEquivalent(expectedCompletions, completions); + } + + [Fact] + public void GetAttributeCompletions_PossibleDescriptorsReturnBoundRequiredAttributesWithExistingCompletions() + { + // Arrange + var documentDescriptors = new[] + { + TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule + .RequireTagName("div") + .RequireAttribute(attribute => attribute.Name("repeat"))) + .BindAttribute(attribute => attribute + .Name("repeat") + .TypeName(typeof(bool).FullName) + .PropertyName("Repeat")) + .BindAttribute(attribute => attribute + .Name("visible") + .TypeName(typeof(bool).FullName) + .PropertyName("Visible")) + .Build(), + TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule + .RequireTagName("*") + .RequireAttribute(attribute => attribute.Name("class"))) + .BindAttribute(attribute => attribute + .Name("class") + .TypeName(typeof(string).FullName) + .PropertyName("Class")) + .Build(), + }; + var expectedCompletions = AttributeCompletionResult.Create(new Dictionary>() + { + ["class"] = new HashSet(documentDescriptors[1].BoundAttributes), + ["onclick"] = new HashSet(), + ["repeat"] = new HashSet() + { + documentDescriptors[0].BoundAttributes.First() + } + }); + + var existingCompletions = new[] { "onclick" }; + var completionContext = BuildAttributeCompletionContext( + documentDescriptors, + existingCompletions, + currentTagName: "div"); + var service = CreateTagHelperCompletionFactsService(); + + // Act + var completions = service.GetAttributeCompletions(completionContext); + + // Assert + AssertCompletionsAreEquivalent(expectedCompletions, completions); + } + + [Fact] + public void GetAttributeCompletions_AppliedDescriptorsReturnAllBoundAttributesWithExistingCompletionsForSchemaTags() + { + // Arrange + var documentDescriptors = new[] + { + TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule.RequireTagName("div")) + .BindAttribute(attribute => attribute + .Name("repeat") + .TypeName(typeof(bool).FullName) + .PropertyName("Repeat")) + .BindAttribute(attribute => attribute + .Name("visible") + .TypeName(typeof(bool).FullName) + .PropertyName("Visible")) + .Build(), + TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule + .RequireTagName("*") + .RequireAttribute(attribute => attribute.Name("class"))) + .BindAttribute(attribute => attribute + .Name("class") + .TypeName(typeof(string).FullName) + .PropertyName("Class")) + .Build(), + TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule.RequireTagName("*")) + .BindAttribute(attribute => attribute + .Name("visible") + .TypeName(typeof(bool).FullName) + .PropertyName("Visible")) + .Build(), + }; + var expectedCompletions = AttributeCompletionResult.Create(new Dictionary>() + { + ["onclick"] = new HashSet(), + ["class"] = new HashSet(documentDescriptors[1].BoundAttributes), + ["repeat"] = new HashSet() + { + documentDescriptors[0].BoundAttributes.First() + }, + ["visible"] = new HashSet() + { + documentDescriptors[0].BoundAttributes.Last(), + documentDescriptors[2].BoundAttributes.First(), + } + }); + + var existingCompletions = new[] { "class", "onclick" }; + var completionContext = BuildAttributeCompletionContext( + documentDescriptors, + existingCompletions, + currentTagName: "div"); + var service = CreateTagHelperCompletionFactsService(); + + // Act + var completions = service.GetAttributeCompletions(completionContext); + + // Assert + AssertCompletionsAreEquivalent(expectedCompletions, completions); + } + + [Fact] + public void GetAttributeCompletions_AppliedTagOutputHintDescriptorsReturnBoundAttributesWithExistingCompletionsForNonSchemaTags() + { + // Arrange + var documentDescriptors = new[] + { + TagHelperDescriptorBuilder.Create("CustomTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule.RequireTagName("custom")) + .BindAttribute(attribute => attribute + .Name("repeat") + .TypeName(typeof(bool).FullName) + .PropertyName("Repeat")) + .TagOutputHint("div") + .Build(), + }; + var expectedCompletions = AttributeCompletionResult.Create(new Dictionary>() + { + ["class"] = new HashSet(), + ["repeat"] = new HashSet(documentDescriptors[0].BoundAttributes) + }); + + var existingCompletions = new[] { "class" }; + var completionContext = BuildAttributeCompletionContext( + documentDescriptors, + existingCompletions, + currentTagName: "custom"); + var service = CreateTagHelperCompletionFactsService(); + + // Act + var completions = service.GetAttributeCompletions(completionContext); + + // Assert + AssertCompletionsAreEquivalent(expectedCompletions, completions); + } + + [Fact] + public void GetAttributeCompletions_AppliedDescriptorsReturnBoundAttributesCompletionsForNonSchemaTags() + { + // Arrange + var documentDescriptors = new[] + { + TagHelperDescriptorBuilder.Create("CustomTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule.RequireTagName("custom")) + .BindAttribute(attribute => attribute + .Name("repeat") + .TypeName(typeof(bool).FullName) + .PropertyName("Repeat")) + .Build(), + }; + var expectedCompletions = AttributeCompletionResult.Create(new Dictionary>() + { + ["repeat"] = new HashSet(documentDescriptors[0].BoundAttributes) + }); + + var existingCompletions = new[] { "class" }; + var completionContext = BuildAttributeCompletionContext( + documentDescriptors, + existingCompletions, + currentTagName: "custom"); + var service = CreateTagHelperCompletionFactsService(); + + // Act + var completions = service.GetAttributeCompletions(completionContext); + + // Assert + AssertCompletionsAreEquivalent(expectedCompletions, completions); + } + + [Fact] + public void GetAttributeCompletions_AppliedDescriptorsReturnBoundAttributesWithExistingCompletionsForSchemaTags() + { + // Arrange + var documentDescriptors = new[] + { + TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule.RequireTagName("div")) + .BindAttribute(attribute => attribute + .Name("repeat") + .TypeName(typeof(bool).FullName) + .PropertyName("Repeat")) + .Build(), + }; + var expectedCompletions = AttributeCompletionResult.Create(new Dictionary>() + { + ["class"] = new HashSet(), + ["repeat"] = new HashSet(documentDescriptors[0].BoundAttributes) + }); + + var existingCompletions = new[] { "class" }; + var completionContext = BuildAttributeCompletionContext( + documentDescriptors, + existingCompletions, + currentTagName: "div"); + var service = CreateTagHelperCompletionFactsService(); + + // Act + var completions = service.GetAttributeCompletions(completionContext); + + // Assert + AssertCompletionsAreEquivalent(expectedCompletions, completions); + } + + [Fact] + public void GetAttributeCompletions_NoDescriptorsReturnsExistingCompletions() + { + // Arrange + var expectedCompletions = AttributeCompletionResult.Create(new Dictionary>() + { + ["class"] = new HashSet(), + }); + + var existingCompletions = new[] { "class" }; + var completionContext = BuildAttributeCompletionContext( + Enumerable.Empty(), + existingCompletions, + currentTagName: "div"); + var service = CreateTagHelperCompletionFactsService(); + + // Act + var completions = service.GetAttributeCompletions(completionContext); + + // Assert + AssertCompletionsAreEquivalent(expectedCompletions, completions); + } + + [Fact] + public void GetAttributeCompletions_NoDescriptorsForUnprefixedTagReturnsExistingCompletions() + { + // Arrange + var documentDescriptors = new[] + { + TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule + .RequireTagName("div") + .RequireAttribute(attribute => attribute.Name("special"))) + .Build(), + }; + var expectedCompletions = AttributeCompletionResult.Create(new Dictionary>() + { + ["class"] = new HashSet(), + }); + + var existingCompletions = new[] { "class" }; + var completionContext = BuildAttributeCompletionContext( + documentDescriptors, + existingCompletions, + currentTagName: "div", + tagHelperPrefix: "th:"); + var service = CreateTagHelperCompletionFactsService(); + + // Act + var completions = service.GetAttributeCompletions(completionContext); + + // Assert + AssertCompletionsAreEquivalent(expectedCompletions, completions); + } + + [Fact] + public void GetAttributeCompletions_NoDescriptorsForTagReturnsExistingCompletions() + { + // Arrange + var documentDescriptors = new[] + { + TagHelperDescriptorBuilder.Create("MyTableTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule + .RequireTagName("table") + .RequireAttribute(attribute => attribute.Name("special"))) + .Build(), + }; + var expectedCompletions = AttributeCompletionResult.Create(new Dictionary>() + { + ["class"] = new HashSet(), + }); + + var existingCompletions = new[] { "class" }; + var completionContext = BuildAttributeCompletionContext( + documentDescriptors, + existingCompletions, + currentTagName: "div"); + var service = CreateTagHelperCompletionFactsService(); + + // Act + var completions = service.GetAttributeCompletions(completionContext); + + // Assert + AssertCompletionsAreEquivalent(expectedCompletions, completions); + } + [Fact] public void GetElementCompletions_TagOutputHintDoesNotFallThroughToSchemaCheck() { @@ -32,7 +425,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor }); var existingCompletions = new[] { "table" }; - var completionContext = BuildCompletionContext( + var completionContext = BuildElementCompletionContext( documentDescriptors, existingCompletions, containingTagName: "body", @@ -66,7 +459,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor }); var existingCompletions = new[] { "li" }; - var completionContext = BuildCompletionContext( + var completionContext = BuildElementCompletionContext( documentDescriptors, existingCompletions, containingTagName: "ul", @@ -101,7 +494,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor }); var existingCompletions = new[] { "li" }; - var completionContext = BuildCompletionContext( + var completionContext = BuildElementCompletionContext( documentDescriptors, existingCompletions, containingTagName: "ul", @@ -135,7 +528,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor }); var existingCompletions = new[] { "li" }; - var completionContext = BuildCompletionContext( + var completionContext = BuildElementCompletionContext( documentDescriptors, existingCompletions, containingTagName: "ul"); @@ -171,7 +564,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor }); var existingCompletions = new[] { "strong", "b", "bold" }; - var completionContext = BuildCompletionContext( + var completionContext = BuildElementCompletionContext( documentDescriptors, existingCompletions, containingTagName: "ul"); @@ -203,7 +596,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor }); var existingCompletions = new[] { "li" }; - var completionContext = BuildCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul"); + var completionContext = BuildElementCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul"); var service = CreateTagHelperCompletionFactsService(); // Act @@ -237,7 +630,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor }); var existingCompletions = new[] { "li" }; - var completionContext = BuildCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul"); + var completionContext = BuildElementCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul"); var service = CreateTagHelperCompletionFactsService(); // Act @@ -269,7 +662,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor }); var existingCompletions = new[] { "li" }; - var completionContext = BuildCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul"); + var completionContext = BuildElementCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul"); var service = CreateTagHelperCompletionFactsService(); // Act @@ -298,7 +691,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor }); var existingCompletions = new[] { "li" }; - var completionContext = BuildCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul"); + var completionContext = BuildElementCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul"); var service = CreateTagHelperCompletionFactsService(); // Act @@ -324,7 +717,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor var expectedCompletions = ElementCompletionResult.Create(new Dictionary>()); var existingCompletions = Enumerable.Empty(); - var completionContext = BuildCompletionContext( + var completionContext = BuildElementCompletionContext( documentDescriptors, existingCompletions, containingTagName: null, @@ -359,7 +752,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor }); var existingCompletions = new[] { "p", "em" }; - var completionContext = BuildCompletionContext(documentDescriptors, existingCompletions, containingTagName: "div"); + var completionContext = BuildElementCompletionContext(documentDescriptors, existingCompletions, containingTagName: "div"); var service = CreateTagHelperCompletionFactsService(); // Act @@ -387,7 +780,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor ["bold"] = new HashSet(), }); - var completionContext = BuildCompletionContext(documentDescriptors, Enumerable.Empty(), containingTagName: "div"); + var completionContext = BuildElementCompletionContext(documentDescriptors, Enumerable.Empty(), containingTagName: "div"); var service = CreateTagHelperCompletionFactsService(); // Act @@ -417,7 +810,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor ["div"] = new HashSet { documentDescriptors[0] } }); - var completionContext = BuildCompletionContext(documentDescriptors, Enumerable.Empty(), containingTagName: "div"); + var completionContext = BuildElementCompletionContext(documentDescriptors, Enumerable.Empty(), containingTagName: "div"); var service = CreateTagHelperCompletionFactsService(); // Act @@ -453,7 +846,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor ["div"] = new HashSet { documentDescriptors[0], documentDescriptors[1] }, }); - var completionContext = BuildCompletionContext(documentDescriptors, Enumerable.Empty(), containingTagName: "div"); + var completionContext = BuildElementCompletionContext(documentDescriptors, Enumerable.Empty(), containingTagName: "div"); var service = CreateTagHelperCompletionFactsService(); // Act @@ -483,7 +876,19 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor } } - private static ElementCompletionContext BuildCompletionContext( + private static void AssertCompletionsAreEquivalent(AttributeCompletionResult expected, AttributeCompletionResult actual) + { + Assert.Equal(expected.Completions.Count, actual.Completions.Count); + + foreach (var expectedCompletion in expected.Completions) + { + var actualValue = actual.Completions[expectedCompletion.Key]; + Assert.NotNull(actualValue); + Assert.Equal(expectedCompletion.Value, actualValue, BoundAttributeDescriptorComparer.CaseSensitive); + } + } + + private static ElementCompletionContext BuildElementCompletionContext( IEnumerable descriptors, IEnumerable existingCompletions, string containingTagName, @@ -501,5 +906,25 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor return completionContext; } + + private static AttributeCompletionContext BuildAttributeCompletionContext( + IEnumerable descriptors, + IEnumerable existingCompletions, + string currentTagName, + IEnumerable> attributes = null, + string tagHelperPrefix = "") + { + attributes = attributes ?? Enumerable.Empty>(); + var documentContext = TagHelperDocumentContext.Create(tagHelperPrefix, descriptors); + var completionContext = new AttributeCompletionContext( + documentContext, + existingCompletions, + currentTagName, + attributes, + currentParentTagName: "body", + inHTMLSchema: (tag) => tag == "strong" || tag == "b" || tag == "bold" || tag == "li" || tag == "div"); + + return completionContext; + } } }