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); }
// 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); }
internal static bool ExpectsBooleanValue(this BoundAttributeDescriptor attribute, string name) { if (attribute.IsBooleanProperty) { return(true); } var isIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(name, attribute); return(isIndexerNameMatch && attribute.IsIndexerBooleanProperty); }
// 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); } }
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); } }
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); }
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); }
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); }
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); } }
// 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); }
/// <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 }); }
// 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(":"); } } }
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();