Example #1
0
        public override IReadOnlyList <TagHelperDescriptor> GetTagHelpersGivenParent(TagHelperDocumentContext documentContext, string parentTag)
        {
            if (documentContext == null)
            {
                throw new ArgumentNullException(nameof(documentContext));
            }

            var matchingDescriptors = new List <TagHelperDescriptor>();
            var descriptors         = documentContext?.TagHelpers;

            if (descriptors?.Count == 0)
            {
                return(matchingDescriptors);
            }

            for (var i = 0; i < descriptors.Count; i++)
            {
                var descriptor = descriptors[i];
                foreach (var rule in descriptor.TagMatchingRules)
                {
                    if (TagHelperMatchingConventions.SatisfiesParentTag(parentTag, rule))
                    {
                        matchingDescriptors.Add(descriptor);
                        break;
                    }
                }
            }

            return(matchingDescriptors);
        }
Example #2
0
        // Finds first TagHelperAttributeDescriptor matching given name.
        private static BoundAttributeDescriptor FindFirstBoundAttribute(
            string name,
            IEnumerable <TagHelperDescriptor> descriptors)
        {
            var firstBoundAttribute = descriptors
                                      .SelectMany(descriptor => descriptor.BoundAttributes)
                                      .FirstOrDefault(attributeDescriptor => TagHelperMatchingConventions.CanSatisfyBoundAttribute(name, attributeDescriptor));

            return(firstBoundAttribute);
        }
Example #3
0
    internal static bool ExpectsBooleanValue(this BoundAttributeDescriptor attribute, string name)
    {
        if (attribute.IsBooleanProperty)
        {
            return(true);
        }

        var isIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(name, attribute);

        return(isIndexerNameMatch && attribute.IsIndexerBooleanProperty);
    }
Example #4
0
        // Determines the full name of the Type of the property corresponding to an attribute with the given name.
        private static string GetPropertyType(string name, IEnumerable <TagHelperDescriptor> descriptors)
        {
            var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors);
            var isBoundToIndexer    = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(name, firstBoundAttribute);

            if (isBoundToIndexer)
            {
                return(firstBoundAttribute?.IndexerTypeName);
            }
            else
            {
                return(firstBoundAttribute?.TypeName);
            }
        }
Example #5
0
        private HoverModel AttributeInfoToHover(IEnumerable <BoundAttributeDescriptor> descriptors, RangeModel range, string attributeName, ClientCapabilities clientCapabilities)
        {
            var descriptionInfos = descriptors.Select(boundAttribute =>
            {
                var indexer         = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(attributeName, boundAttribute);
                var descriptionInfo = BoundAttributeDescriptionInfo.From(boundAttribute, indexer);
                return(descriptionInfo);
            }).ToList().AsReadOnly();
            var attrDescriptionInfo = new AggregateBoundAttributeDescription(descriptionInfos);

            var isVSClient = clientCapabilities is PlatformAgnosticClientCapabilities platformAgnosticClientCapabilities &&
                             platformAgnosticClientCapabilities.SupportsVisualStudioExtensions;

            if (isVSClient && _vsLspTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, out ContainerElement classifiedTextElement))
            {
                var vsHover = new OmniSharpVSHover
                {
                    Contents   = new MarkedStringsOrMarkupContent(),
                    Range      = range,
                    RawContent = classifiedTextElement,
                };

                return(vsHover);
            }
            else
            {
                var hoverContentFormat = GetHoverContentFormat(clientCapabilities);

                if (!_lspTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, hoverContentFormat, out var vsMarkupContent))
                {
                    return(null);
                }

                Enum.TryParse(vsMarkupContent.Kind.Value, out MarkupKind markupKind);

                var markupContent = new MarkupContent()
                {
                    Value = vsMarkupContent.Value,
                    Kind  = markupKind,
                };

                var hover = new HoverModel
                {
                    Contents = new MarkedStringsOrMarkupContent(markupContent),
                    Range    = range
                };

                return(hover);
            }
        }
Example #6
0
        public override IReadOnlyList <TagHelperDescriptor> GetTagHelpersGivenTag(
            TagHelperDocumentContext documentContext,
            string tagName,
            string parentTag)
        {
            if (documentContext == null)
            {
                throw new ArgumentNullException(nameof(documentContext));
            }

            if (tagName == null)
            {
                throw new ArgumentNullException(nameof(tagName));
            }

            var matchingDescriptors = new List <TagHelperDescriptor>();
            var descriptors         = documentContext?.TagHelpers;

            if (descriptors?.Count == 0)
            {
                return(matchingDescriptors);
            }

            var prefix = documentContext.Prefix ?? string.Empty;

            if (!tagName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
            {
                // Can't possibly match TagHelpers, it doesn't start with the TagHelperPrefix.
                return(matchingDescriptors);
            }

            var tagNameWithoutPrefix = tagName.Substring(prefix.Length);

            for (var i = 0; i < descriptors.Count; i++)
            {
                var descriptor = descriptors[i];
                foreach (var rule in descriptor.TagMatchingRules)
                {
                    if (TagHelperMatchingConventions.SatisfiesTagName(tagNameWithoutPrefix, rule) &&
                        TagHelperMatchingConventions.SatisfiesParentTag(parentTag, rule))
                    {
                        matchingDescriptors.Add(descriptor);
                        break;
                    }
                }
            }

            return(matchingDescriptors);
        }
    public void CanSatisfyBoundAttribute_IndexerAttribute_ReturnsTrueIfMatching()
    {
        // Arrange
        var tagHelperBuilder = new DefaultTagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "TestTagHelper", "Test");
        var builder          = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind);

        builder.AsDictionary("asp-", typeof(Dictionary <string, string>).FullName);

        var boundAttribute = builder.Build();

        // Act
        var result = TagHelperMatchingConventions.CanSatisfyBoundAttribute("asp-route-controller", boundAttribute);

        // Assert
        Assert.True(result);
    }
        private Dictionary <string, TagHelperPair> FindMatchingTagHelpers(RazorCodeActionContext context, MarkupStartTagSyntax startTag)
        {
            // Get all data necessary for matching
            var    tagName       = startTag.Name.Content;
            string parentTagName = null;

            if (startTag.Parent?.Parent is MarkupElementSyntax parentElement)
            {
                parentTagName = parentElement.StartTag.Name.Content;
            }
            else if (startTag.Parent?.Parent is MarkupTagHelperElementSyntax parentTagHelperElement)
            {
                parentTagName = parentTagHelperElement.StartTag.Name.Content;
            }

            var attributes = _tagHelperFactsService.StringifyAttributes(startTag.Attributes);

            // Find all matching tag helpers
            var matching = new Dictionary <string, TagHelperPair>();

            foreach (var tagHelper in context.DocumentSnapshot.Project.TagHelpers)
            {
                if (tagHelper.TagMatchingRules.All(rule => TagHelperMatchingConventions.SatisfiesRule(tagName, parentTagName, attributes, rule)))
                {
                    matching.Add(tagHelper.Name, new TagHelperPair {
                        Short = tagHelper
                    });
                }
            }

            // Iterate and find the fully qualified version
            foreach (var tagHelper in context.DocumentSnapshot.Project.TagHelpers)
            {
                if (matching.TryGetValue(tagHelper.Name, out var tagHelperPair))
                {
                    if (tagHelperPair != null && tagHelper != tagHelperPair.Short)
                    {
                        tagHelperPair.FullyQualified = tagHelper;
                    }
                }
            }

            return(matching);
        }
        // Determines the full name of the Type of the property corresponding to an attribute with the given name.
        private static string GetPropertyType(string name, IEnumerable <TagHelperDescriptor> descriptors)
        {
            foreach (var descriptor in descriptors)
            {
                if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(name, descriptor, out var firstBoundAttribute, out var indexerMatch, out var _, out var _))
                {
                    if (indexerMatch)
                    {
                        return(firstBoundAttribute.IndexerTypeName);
                    }
                    else
                    {
                        return(firstBoundAttribute.TypeName);
                    }
                }
            }

            return(null);
        }
    public void Matches_ReturnsExpectedResult(
        Action <RequiredAttributeDescriptorBuilder> configure,
        string attributeName,
        string attributeValue,
        bool expectedResult)
    {
        // Arrange
        var tagHelperBuilder       = new DefaultTagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "TestTagHelper", "Test");
        var tagMatchingRuleBuilder = new DefaultTagMatchingRuleDescriptorBuilder(tagHelperBuilder);
        var builder = new DefaultRequiredAttributeDescriptorBuilder(tagMatchingRuleBuilder);

        configure(builder);

        var requiredAttibute = builder.Build();

        // Act
        var result = TagHelperMatchingConventions.SatisfiesRequiredAttribute(attributeName, attributeValue, requiredAttibute);

        // Assert
        Assert.Equal(expectedResult, result);
    }
Example #11
0
        public override IEnumerable <BoundAttributeDescriptor> GetBoundTagHelperAttributes(
            TagHelperDocumentContext documentContext,
            string attributeName,
            TagHelperBinding binding)
        {
            if (documentContext == null)
            {
                throw new ArgumentNullException(nameof(documentContext));
            }

            if (attributeName == null)
            {
                throw new ArgumentNullException(nameof(attributeName));
            }

            if (binding == null)
            {
                throw new ArgumentNullException(nameof(binding));
            }

            var matchingBoundAttributes = new List <BoundAttributeDescriptor>();

            foreach (var descriptor in binding.Descriptors)
            {
                foreach (var boundAttributeDescriptor in descriptor.BoundAttributes)
                {
                    if (TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, boundAttributeDescriptor))
                    {
                        matchingBoundAttributes.Add(boundAttributeDescriptor);

                        // Only one bound attribute can match an attribute
                        break;
                    }
                }
            }

            return(matchingBoundAttributes);
        }
Example #12
0
        private HoverModel AttributeInfoToHover(IEnumerable <BoundAttributeDescriptor> descriptors, RangeModel range, string attributeName)
        {
            var descriptionInfos = descriptors.Select(boundAttribute =>
            {
                var indexer         = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(attributeName, boundAttribute);
                var descriptionInfo = BoundAttributeDescriptionInfo.From(boundAttribute, indexer);
                return(descriptionInfo);
            }).ToList().AsReadOnly();
            var attrDescriptionInfo = new AggregateBoundAttributeDescription(descriptionInfos);

            if (!_tagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, out var markupContent))
            {
                return(null);
            }

            var hover = new HoverModel
            {
                Contents = new MarkedStringsOrMarkupContent(markupContent),
                Range    = range
            };

            return(hover);
        }
Example #13
0
        public override ElementCompletionResult GetElementCompletions(ElementCompletionContext completionContext)
        {
            if (completionContext == null)
            {
                throw new ArgumentNullException(nameof(completionContext));
            }

            var elementCompletions = new Dictionary <string, HashSet <TagHelperDescriptor> >(StringComparer.OrdinalIgnoreCase);

            AddAllowedChildrenCompletions(completionContext, elementCompletions);

            if (elementCompletions.Count > 0)
            {
                // If the containing element is already a TagHelper and only allows certain children.
                var emptyResult = ElementCompletionResult.Create(elementCompletions);
                return(emptyResult);
            }

            elementCompletions = completionContext.ExistingCompletions.ToDictionary(
                completion => completion,
                _ => new HashSet <TagHelperDescriptor>(),
                StringComparer.Ordinal);

            var catchAllDescriptors = new HashSet <TagHelperDescriptor>();
            var prefix = completionContext.DocumentContext.Prefix ?? string.Empty;
            var possibleChildDescriptors = _tagHelperFactsService.GetTagHelpersGivenParent(completionContext.DocumentContext, completionContext.ContainingTagName);

            foreach (var possibleDescriptor in possibleChildDescriptors)
            {
                var addRuleCompletions = false;
                var outputHint         = possibleDescriptor.TagOutputHint;

                foreach (var rule in possibleDescriptor.TagMatchingRules)
                {
                    if (!TagHelperMatchingConventions.SatisfiesParentTag(completionContext.ContainingTagName, rule))
                    {
                        continue;
                    }

                    if (rule.TagName == TagHelperMatchingConventions.ElementCatchAllName)
                    {
                        catchAllDescriptors.Add(possibleDescriptor);
                    }
                    else if (elementCompletions.ContainsKey(rule.TagName))
                    {
                        addRuleCompletions = true;
                    }
                    else if (outputHint != null)
                    {
                        // If the current descriptor has an output hint we need to make sure it shows up only when its output hint would normally show up.
                        // Example: We have a MyTableTagHelper that has an output hint of "table" and a MyTrTagHelper that has an output hint of "tr".
                        // If we try typing in a situation like this: <body > | </body>
                        // We'd expect to only get "my-table" as a completion because the "body" tag doesn't allow "tr" tags.
                        addRuleCompletions = elementCompletions.ContainsKey(outputHint);
                    }
                    else if (!completionContext.InHTMLSchema(rule.TagName) || rule.TagName.Any(c => char.IsUpper(c)))
                    {
                        // If there is an unknown HTML schema tag that doesn't exist in the current completion we should add it. This happens for
                        // TagHelpers that target non-schema oriented tags.
                        // The second condition is a workaround for the fact that InHTMLSchema does a case insensitive comparison.
                        // We want completions to not dedupe by casing. E.g, we want to show both <div> and <DIV> completion items separately.
                        addRuleCompletions = true;
                    }

                    if (addRuleCompletions)
                    {
                        UpdateCompletions(prefix + rule.TagName, possibleDescriptor);
                    }
                }
            }

            // We needed to track all catch-alls and update their completions after all other completions have been completed.
            // This way, any TagHelper added completions will also have catch-alls listed under their entries.
            foreach (var catchAllDescriptor in catchAllDescriptors)
            {
                foreach (var completionTagName in elementCompletions.Keys)
                {
                    if (elementCompletions[completionTagName].Count > 0 ||
                        !string.IsNullOrEmpty(prefix) && completionTagName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                    {
                        // The current completion either has other TagHelper's associated with it or is prefixed with a non-empty
                        // TagHelper prefix.
                        UpdateCompletions(completionTagName, catchAllDescriptor);
                    }
                }
            }

            var result = ElementCompletionResult.Create(elementCompletions);

            return(result);

            void UpdateCompletions(string tagName, TagHelperDescriptor possibleDescriptor)
            {
                if (!elementCompletions.TryGetValue(tagName, out var existingRuleDescriptors))
                {
                    existingRuleDescriptors     = new HashSet <TagHelperDescriptor>();
                    elementCompletions[tagName] = existingRuleDescriptors;
                }

                existingRuleDescriptors.Add(possibleDescriptor);
            }
        }
Example #14
0
        // Internal for testing
        internal IReadOnlyList <RazorCompletionItem> GetAttributeParameterCompletions(
            string attributeName,
            string parameterName,
            string containingTagName,
            IEnumerable <string> attributes,
            TagHelperDocumentContext tagHelperDocumentContext)
        {
            var descriptorsForTag = _tagHelperFactsService.GetTagHelpersGivenTag(tagHelperDocumentContext, containingTagName, parentTag: null);

            if (descriptorsForTag.Count == 0)
            {
                // If the current tag has no possible descriptors then we can't have any additional attributes.
                return(Array.Empty <RazorCompletionItem>());
            }

            // Attribute parameters are case sensitive when matching
            var attributeCompletions = new Dictionary <string, HashSet <AttributeDescriptionInfo> >(StringComparer.Ordinal);

            foreach (var descriptor in descriptorsForTag)
            {
                for (var i = 0; i < descriptor.BoundAttributes.Count; i++)
                {
                    var attributeDescriptor      = descriptor.BoundAttributes[i];
                    var boundAttributeParameters = attributeDescriptor.BoundAttributeParameters;
                    if (boundAttributeParameters.Count == 0)
                    {
                        continue;
                    }

                    if (TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, attributeDescriptor))
                    {
                        for (var j = 0; j < boundAttributeParameters.Count; j++)
                        {
                            var parameterDescriptor = boundAttributeParameters[j];

                            if (attributes.Any(name => TagHelperMatchingConventions.SatisfiesBoundAttributeWithParameter(name, attributeDescriptor, parameterDescriptor)))
                            {
                                // There's already an existing attribute that satisfies this parameter, don't show it in the completion list.
                                continue;
                            }

                            if (!attributeCompletions.TryGetValue(parameterDescriptor.Name, out var attributeDescriptionInfos))
                            {
                                attributeDescriptionInfos = new HashSet <AttributeDescriptionInfo>();
                                attributeCompletions[parameterDescriptor.Name] = attributeDescriptionInfos;
                            }

                            var descriptionInfo = new AttributeDescriptionInfo(
                                parameterDescriptor.TypeName,
                                descriptor.GetTypeName(),
                                parameterDescriptor.GetPropertyName(),
                                parameterDescriptor.Documentation);
                            attributeDescriptionInfos.Add(descriptionInfo);
                        }
                    }
                }
            }

            var completionItems = new List <RazorCompletionItem>();

            foreach (var completion in attributeCompletions)
            {
                if (string.Equals(completion.Key, parameterName, StringComparison.Ordinal))
                {
                    // This completion is identical to the selected parameter, don't provide for completions for what's already
                    // present in the document.
                    continue;
                }

                var razorCompletionItem = new RazorCompletionItem(
                    completion.Key,
                    completion.Key,
                    RazorCompletionItemKind.DirectiveAttributeParameter);
                var completionDescription = new AttributeCompletionDescription(completion.Value.ToArray());
                razorCompletionItem.SetAttributeCompletionDescription(completionDescription);

                completionItems.Add(razorCompletionItem);
            }

            return(completionItems);
        }
Example #15
0
    /// <summary>
    /// Gets all tag helpers that match the given HTML tag criteria.
    /// </summary>
    /// <param name="tagName">The name of the HTML tag to match. Providing a '*' tag name
    /// retrieves catch-all <see cref="TagHelperDescriptor"/>s (descriptors that target every tag).</param>
    /// <param name="attributes">Attributes on the HTML tag.</param>
    /// <param name="parentTagName">The parent tag name of the given <paramref name="tagName"/> tag.</param>
    /// <param name="parentIsTagHelper">Is the parent tag of the given <paramref name="tagName"/> tag a tag helper.</param>
    /// <returns><see cref="TagHelperDescriptor"/>s that apply to the given HTML tag criteria.
    /// Will return <c>null</c> if no <see cref="TagHelperDescriptor"/>s are a match.</returns>
    public TagHelperBinding GetBinding(
        string tagName,
        IReadOnlyList <KeyValuePair <string, string> > attributes,
        string parentTagName,
        bool parentIsTagHelper)
    {
        if (!string.IsNullOrEmpty(_tagHelperPrefix) &&
            (tagName.Length <= _tagHelperPrefix.Length ||
             !tagName.StartsWith(_tagHelperPrefix, StringComparison.OrdinalIgnoreCase)))
        {
            // The tagName doesn't have the tag helper prefix, we can short circuit.
            return(null);
        }

        IEnumerable <TagHelperDescriptor> descriptors;

        // Ensure there's a HashSet to use.
        if (!_registrations.TryGetValue(TagHelperMatchingConventions.ElementCatchAllName, out HashSet <TagHelperDescriptor> catchAllDescriptors))
        {
            descriptors = new HashSet <TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
        }
        else
        {
            descriptors = catchAllDescriptors;
        }

        // If we have a tag name associated with the requested name, we need to combine matchingDescriptors
        // with all the catch-all descriptors.
        if (_registrations.TryGetValue(tagName, out HashSet <TagHelperDescriptor> matchingDescriptors))
        {
            descriptors = matchingDescriptors.Concat(descriptors);
        }

        var           tagNameWithoutPrefix       = _tagHelperPrefix != null ? new StringSegment(tagName, _tagHelperPrefix.Length) : tagName;
        StringSegment parentTagNameWithoutPrefix = parentTagName;

        if (_tagHelperPrefix != null && parentIsTagHelper)
        {
            parentTagNameWithoutPrefix = new StringSegment(parentTagName, _tagHelperPrefix.Length);
        }

        Dictionary <TagHelperDescriptor, IReadOnlyList <TagMatchingRuleDescriptor> > applicableDescriptorMappings = null;

        foreach (var descriptor in descriptors)
        {
            // We're avoiding desccriptor.TagMatchingRules.Where and applicableRules.Any() to avoid
            // Enumerator allocations on this hotpath
            List <TagMatchingRuleDescriptor> applicableRules = null;
            for (var i = 0; i < descriptor.TagMatchingRules.Count; i++)
            {
                var rule = descriptor.TagMatchingRules[i];
                if (TagHelperMatchingConventions.SatisfiesRule(tagNameWithoutPrefix, parentTagNameWithoutPrefix, attributes, rule))
                {
                    if (applicableRules is null)
                    {
                        applicableRules = new List <TagMatchingRuleDescriptor>();
                    }

                    applicableRules.Add(rule);
                }
            }

            if (applicableRules != null && applicableRules.Count > 0)
            {
                if (applicableDescriptorMappings == null)
                {
                    applicableDescriptorMappings = new Dictionary <TagHelperDescriptor, IReadOnlyList <TagMatchingRuleDescriptor> >();
                }

                applicableDescriptorMappings[descriptor] = applicableRules;
            }
        }

        if (applicableDescriptorMappings == null)
        {
            return(null);
        }

        var tagHelperBinding = new TagHelperBinding(
            tagName,
            attributes,
            parentTagName,
            applicableDescriptorMappings,
            _tagHelperPrefix);

        return(tagHelperBinding);
    }
        // Create a TryParseResult for given name, filling in binding details.
        private static TryParseResult CreateTryParseResult(
            string name,
            IEnumerable <TagHelperDescriptor> descriptors,
            HashSet <string> processedBoundAttributeNames)
        {
            var isBoundAttribute          = false;
            var isBoundNonStringAttribute = false;
            var isBoundBooleanAttribute   = false;
            var isMissingDictionaryKey    = false;
            var isDirectiveAttribute      = false;

            foreach (var descriptor in descriptors)
            {
                if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(
                        name,
                        descriptor,
                        out var firstBoundAttribute,
                        out var indexerMatch,
                        out var parameterMatch,
                        out var boundAttributeParameter))
                {
                    isBoundAttribute = true;
                    if (parameterMatch)
                    {
                        isBoundNonStringAttribute = !boundAttributeParameter.IsStringProperty;
                        isBoundBooleanAttribute   = boundAttributeParameter.IsBooleanProperty;
                        isMissingDictionaryKey    = false;
                    }
                    else
                    {
                        isBoundNonStringAttribute = !firstBoundAttribute.ExpectsStringValue(name);
                        isBoundBooleanAttribute   = firstBoundAttribute.ExpectsBooleanValue(name);
                        isMissingDictionaryKey    = firstBoundAttribute.IndexerNamePrefix != null &&
                                                    name.Length == firstBoundAttribute.IndexerNamePrefix.Length;
                    }

                    isDirectiveAttribute = firstBoundAttribute.IsDirectiveAttribute();

                    break;
                }
            }

            var isDuplicateAttribute = false;

            if (isBoundAttribute && !processedBoundAttributeNames.Add(name))
            {
                // A bound attribute with the same name has already been processed.
                isDuplicateAttribute = true;
            }

            return(new TryParseResult
            {
                AttributeName = name,
                IsBoundAttribute = isBoundAttribute,
                IsBoundNonStringAttribute = isBoundNonStringAttribute,
                IsBoundBooleanAttribute = isBoundBooleanAttribute,
                IsMissingDictionaryKey = isMissingDictionaryKey,
                IsDuplicateAttribute = isDuplicateAttribute,
                IsDirectiveAttribute = isDirectiveAttribute
            });
        }
Example #17
0
        // Internal for testing
        internal IReadOnlyList <RazorCompletionItem> GetAttributeCompletions(
            string selectedAttributeName,
            string containingTagName,
            IEnumerable <string> attributes,
            TagHelperDocumentContext tagHelperDocumentContext)
        {
            var descriptorsForTag = _tagHelperFactsService.GetTagHelpersGivenTag(tagHelperDocumentContext, containingTagName, parentTag: null);

            if (descriptorsForTag.Count == 0)
            {
                // If the current tag has no possible descriptors then we can't have any directive attributes.
                return(Array.Empty <RazorCompletionItem>());
            }

            // Attributes are case sensitive when matching
            var attributeCompletions = new Dictionary <string, (HashSet <AttributeDescriptionInfo>, HashSet <string>)>(StringComparer.Ordinal);

            for (var i = 0; i < descriptorsForTag.Count; i++)
            {
                var descriptor = descriptorsForTag[i];

                foreach (var attributeDescriptor in descriptor.BoundAttributes)
                {
                    if (!attributeDescriptor.IsDirectiveAttribute())
                    {
                        // We don't care about non-directive attributes
                        continue;
                    }

                    if (!TryAddCompletion(attributeDescriptor.Name, attributeDescriptor, descriptor) && attributeDescriptor.BoundAttributeParameters.Count > 0)
                    {
                        // This attribute has parameters and the base attribute name (@bind) is already satisfied. We need to check if there are any valid
                        // parameters left to be provided, if so, we need to still represent the base attribute name in the completion list.

                        for (var j = 0; j < attributeDescriptor.BoundAttributeParameters.Count; j++)
                        {
                            var parameterDescriptor = attributeDescriptor.BoundAttributeParameters[j];
                            if (!attributes.Any(name => TagHelperMatchingConventions.SatisfiesBoundAttributeWithParameter(name, attributeDescriptor, parameterDescriptor)))
                            {
                                // This bound attribute parameter has not had a completion entry added for it, re-represent the base attribute name in the completion list
                                AddCompletion(attributeDescriptor.Name, attributeDescriptor, descriptor);
                                break;
                            }
                        }
                    }

                    if (!string.IsNullOrEmpty(attributeDescriptor.IndexerNamePrefix))
                    {
                        TryAddCompletion(attributeDescriptor.IndexerNamePrefix + "...", attributeDescriptor, descriptor);
                    }
                }
            }

            var completionItems = new List <RazorCompletionItem>();

            foreach (var completion in attributeCompletions)
            {
                var insertText = completion.Key;
                if (insertText.EndsWith("..."))
                {
                    // Indexer attribute, we don't want to insert with the triple dot.
                    insertText = insertText.Substring(0, insertText.Length - 3);
                }

                if (insertText.StartsWith("@"))
                {
                    // Strip off the @ from the insertion text. This change is here to align the insertion text with the
                    // completion hooks into VS and VSCode. Basically, completion triggers when `@` is typed so we don't
                    // want to insert `@bind` because `@` already exists.
                    insertText = insertText.Substring(1);
                }

                (var attributeDescriptionInfos, var commitCharacters) = completion.Value;

                var razorCompletionItem = new RazorCompletionItem(
                    completion.Key,
                    insertText,
                    RazorCompletionItemKind.DirectiveAttribute,
                    commitCharacters);
                var completionDescription = new AttributeCompletionDescription(attributeDescriptionInfos.ToArray());
                razorCompletionItem.SetAttributeCompletionDescription(completionDescription);

                completionItems.Add(razorCompletionItem);
            }

            return(completionItems);

            bool TryAddCompletion(string attributeName, BoundAttributeDescriptor boundAttributeDescriptor, TagHelperDescriptor tagHelperDescriptor)
            {
                if (attributes.Any(name => string.Equals(name, attributeName, StringComparison.Ordinal)) &&
                    !string.Equals(selectedAttributeName, attributeName, StringComparison.Ordinal))
                {
                    // Attribute is already present on this element and it is not the selected attribute.
                    // It shouldn't exist in the completion list.
                    return(false);
                }

                AddCompletion(attributeName, boundAttributeDescriptor, tagHelperDescriptor);
                return(true);
            }

            void AddCompletion(string attributeName, BoundAttributeDescriptor boundAttributeDescriptor, TagHelperDescriptor tagHelperDescriptor)
            {
                if (!attributeCompletions.TryGetValue(attributeName, out var attributeDetails))
                {
                    attributeDetails = (new HashSet <AttributeDescriptionInfo>(), new HashSet <string>());
                    attributeCompletions[attributeName] = attributeDetails;
                }

                (var attributeDescriptionInfos, var commitCharacters) = attributeDetails;

                var descriptionInfo = new AttributeDescriptionInfo(
                    boundAttributeDescriptor.TypeName,
                    tagHelperDescriptor.GetTypeName(),
                    boundAttributeDescriptor.GetPropertyName(),
                    boundAttributeDescriptor.Documentation);

                attributeDescriptionInfos.Add(descriptionInfo);

                if (attributeName.EndsWith("..."))
                {
                    // Indexer attribute, we don't want to commit with standard chars
                    return;
                }

                commitCharacters.Add("=");

                if (tagHelperDescriptor.BoundAttributes.Any(b => b.IsBooleanProperty))
                {
                    commitCharacters.Add(" ");
                }

                if (tagHelperDescriptor.BoundAttributes.Any(b => b.BoundAttributeParameters.Count > 0))
                {
                    commitCharacters.Add(":");
                }
            }
        }
Example #18
0
        internal IReadOnlyList <RazorCompletionItem> GetAttributeCompletions(
            string selectedAttributeName,
            string containingTagName,
            IEnumerable <string> attributes,
            TagHelperDocumentContext tagHelperDocumentContext)
        {
            var descriptorsForTag = _tagHelperFactsService.GetTagHelpersGivenTag(tagHelperDocumentContext, containingTagName, parentTag: null);

            if (descriptorsForTag.Count == 0)
            {
                // If the current tag has no possible descriptors then we can't have any directive attributes.
                return(Array.Empty <RazorCompletionItem>());
            }

            // Attributes are case sensitive when matching
            var attributeCompletions = new Dictionary <string, (HashSet <BoundAttributeDescriptionInfo>, HashSet <string>)>(StringComparer.Ordinal);

            for (var i = 0; i < descriptorsForTag.Count; i++)
            {
                var descriptor = descriptorsForTag[i];

                foreach (var attributeDescriptor in descriptor.BoundAttributes)
                {
                    if (!attributeDescriptor.IsDirectiveAttribute())
                    {
                        // We don't care about non-directive attributes
                        continue;
                    }

                    if (!TryAddCompletion(attributeDescriptor.Name, attributeDescriptor, descriptor) && attributeDescriptor.BoundAttributeParameters.Count > 0)
                    {
                        // This attribute has parameters and the base attribute name (@bind) is already satisfied. We need to check if there are any valid
                        // parameters left to be provided, if so, we need to still represent the base attribute name in the completion list.

                        for (var j = 0; j < attributeDescriptor.BoundAttributeParameters.Count; j++)
                        {
                            var parameterDescriptor = attributeDescriptor.BoundAttributeParameters[j];
                            if (!attributes.Any(name => TagHelperMatchingConventions.SatisfiesBoundAttributeWithParameter(name, attributeDescriptor, parameterDescriptor)))
                            {
                                // This bound attribute parameter has not had a completion entry added for it, re-represent the base attribute name in the completion list
                                AddCompletion(attributeDescriptor.Name, attributeDescriptor, descriptor);
                                break;
                            }
                        }
                    }

                    if (!string.IsNullOrEmpty(attributeDescriptor.IndexerNamePrefix))
                    {
                        TryAddCompletion(attributeDescriptor.IndexerNamePrefix + "...", attributeDescriptor, descriptor);
                    }
                }
            }

            var completionItems = new List <RazorCompletionItem>();

            foreach (var completion in attributeCompletions)
            {
                var insertText = completion.Key;
                if (insertText.EndsWith("...", StringComparison.Ordinal))
                {
                    // Indexer attribute, we don't want to insert with the triple dot.
                    insertText = insertText.Substring(0, insertText.Length - 3);
                }

                if (insertText.StartsWith("@", StringComparison.Ordinal))
                {
                    // Strip off the @ from the insertion text. This change is here to align the insertion text with the
                    // completion hooks into VS and VSCode. Basically, completion triggers when `@` is typed so we don't
                    // want to insert `@bind` because `@` already exists.
                    insertText = insertText.Substring(1);
                }

                var(attributeDescriptionInfos, commitCharacters) = completion.Value;
                var razorCommitCharacters = commitCharacters.Select(static c => new RazorCommitCharacter(c)).ToList();