public void GetAttributeCompletions_NoDescriptorsForTagReturnsExistingCompletions()
        {
            // Arrange
            var documentDescriptors = new[]
            {
                TagHelperDescriptorBuilder.Create("MyTableTagHelper", "TestAssembly")
                .TagMatchingRuleDescriptor(rule => rule
                                           .RequireTagName("table")
                                           .RequireAttributeDescriptor(attribute => attribute.Name("special")))
                .Build(),
            };
            var expectedCompletions = AttributeCompletionResult.Create(new Dictionary <string, HashSet <BoundAttributeDescriptor> >()
            {
                ["class"] = new HashSet <BoundAttributeDescriptor>(),
            });

            var existingCompletions = new[] { "class" };
            var completionContext   = BuildAttributeCompletionContext(
                documentDescriptors,
                existingCompletions,
                currentTagName: "div");
            var service = CreateTagHelperCompletionFactsService();

            // Act
            var completions = service.GetAttributeCompletions(completionContext);

            // Assert
            AssertCompletionsAreEquivalent(expectedCompletions, completions);
        }
        public void GetAttributeCompletions_AppliedDescriptorsReturnBoundAttributesWithExistingCompletionsForSchemaTags()
        {
            // Arrange
            var documentDescriptors = new[]
            {
                TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
                .TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
                .BoundAttributeDescriptor(attribute => attribute
                                          .Name("repeat")
                                          .TypeName(typeof(bool).FullName)
                                          .PropertyName("Repeat"))
                .Build(),
            };
            var expectedCompletions = AttributeCompletionResult.Create(new Dictionary <string, HashSet <BoundAttributeDescriptor> >()
            {
                ["class"]  = new HashSet <BoundAttributeDescriptor>(),
                ["repeat"] = new HashSet <BoundAttributeDescriptor>(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);
        }
        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);
            }
        }
        public void GetAttributeCompletions_DoesNotReturnCompletionsForAlreadySuppliedAttributes()
        {
            // Arrange
            var documentDescriptors = new[]
            {
                TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
                .TagMatchingRuleDescriptor(rule => rule
                                           .RequireTagName("div")
                                           .RequireAttributeDescriptor(attribute => attribute.Name("repeat")))
                .BoundAttributeDescriptor(attribute => attribute
                                          .Name("visible")
                                          .TypeName(typeof(bool).FullName)
                                          .PropertyName("Visible"))
                .Build(),
                TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly")
                .TagMatchingRuleDescriptor(rule => rule.RequireTagName("*"))
                .BoundAttributeDescriptor(attribute => attribute
                                          .Name("class")
                                          .TypeName(typeof(string).FullName)
                                          .PropertyName("Class"))
                .Build(),
            };
            var expectedCompletions = AttributeCompletionResult.Create(new Dictionary <string, HashSet <BoundAttributeDescriptor> >()
            {
                ["onclick"] = new HashSet <BoundAttributeDescriptor>(),
                ["visible"] = new HashSet <BoundAttributeDescriptor>()
                {
                    documentDescriptors[0].BoundAttributes.Last()
                }
            });

            var existingCompletions = new[] { "onclick" };
            var completionContext   = BuildAttributeCompletionContext(
                documentDescriptors,
                existingCompletions,
                attributes: new Dictionary <string, string>()
            {
                ["class"]  = "something",
                ["repeat"] = "4"
            },
                currentTagName: "div");
            var service = CreateTagHelperCompletionFactsService();

            // Act
            var completions = service.GetAttributeCompletions(completionContext);

            // Assert
            AssertCompletionsAreEquivalent(expectedCompletions, completions);
        }
        public void GetAttributeCompletions_NoDescriptorsReturnsExistingCompletions()
        {
            // Arrange
            var expectedCompletions = AttributeCompletionResult.Create(new Dictionary <string, HashSet <BoundAttributeDescriptor> >()
            {
                ["class"] = new HashSet <BoundAttributeDescriptor>(),
            });

            var existingCompletions = new[] { "class" };
            var completionContext   = BuildAttributeCompletionContext(
                Enumerable.Empty <TagHelperDescriptor>(),
                existingCompletions,
                currentTagName: "div");
            var service = CreateTagHelperCompletionFactsService();

            // Act
            var completions = service.GetAttributeCompletions(completionContext);

            // Assert
            AssertCompletionsAreEquivalent(expectedCompletions, completions);
        }
        public void GetAttributeCompletions_PossibleDescriptorsReturnUnboundRequiredAttributesWithExistingCompletions()
        {
            // Arrange
            var documentDescriptors = new[]
            {
                TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
                .TagMatchingRuleDescriptor(rule => rule
                                           .RequireTagName("div")
                                           .RequireAttributeDescriptor(attribute => attribute.Name("repeat")))
                .Build(),
                TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly")
                .TagMatchingRuleDescriptor(rule => rule
                                           .RequireTagName("*")
                                           .RequireAttributeDescriptor(attribute => attribute.Name("class")))
                .Build(),
            };
            var expectedCompletions = AttributeCompletionResult.Create(new Dictionary <string, HashSet <BoundAttributeDescriptor> >()
            {
                ["class"]   = new HashSet <BoundAttributeDescriptor>(),
                ["onclick"] = new HashSet <BoundAttributeDescriptor>(),
                ["repeat"]  = new HashSet <BoundAttributeDescriptor>()
            });

            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);
        }
        /*
         * 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 <BoundAttributeDescriptor>(),
                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 <TagHelperDescriptor>();
            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 <BoundAttributeDescriptor>();
                    attributeCompletions[attributeName] = rules;
                }

                if (possibleDescriptor != null)
                {
                    rules.Add(possibleDescriptor);
                }
            }
        }