diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperCompletionService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperCompletionService.cs index 9fb24c0dcb..01c43fdfb4 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperCompletionService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperCompletionService.cs @@ -44,18 +44,20 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor _ => new HashSet(), StringComparer.OrdinalIgnoreCase); + var catchAllDescriptors = new HashSet(); var possibleChildDescriptors = _tagHelperFactsService.GetTagHelpersGivenParent(completionContext.DocumentContext, completionContext.ContainingTagName); foreach (var possibleDescriptor in possibleChildDescriptors) { var addRuleCompletions = false; var outputHint = possibleDescriptor.TagOutputHint; - // Filter out catch-all rules because TagHelpers that target attributes only would light up every child tag otherwise. Force those TagHelpers - // to have additional requirements before showing them in the element completion list. - var nonCatchAllRules = possibleDescriptor.TagMatchingRules.Where(rule => rule.TagName != TagHelperMatchingConventions.ElementCatchAllName); - foreach (var rule in nonCatchAllRules) + foreach (var rule in possibleDescriptor.TagMatchingRules) { - if (elementCompletions.ContainsKey(rule.TagName)) + if (rule.TagName == TagHelperMatchingConventions.ElementCatchAllName) + { + catchAllDescriptors.Add(possibleDescriptor); + } + else if (elementCompletions.ContainsKey(rule.TagName)) { addRuleCompletions = true; } @@ -74,19 +76,35 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor if (addRuleCompletions) { - if (!elementCompletions.TryGetValue(rule.TagName, out var existingRuleDescriptors)) - { - existingRuleDescriptors = new HashSet(); - elementCompletions[rule.TagName] = existingRuleDescriptors; - } - - existingRuleDescriptors.Add(possibleDescriptor); + UpdateCompletions(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 completionTagNames in elementCompletions.Keys) + { + UpdateCompletions(completionTagNames, catchAllDescriptor); + } + } + var result = ElementCompletionResult.Create(elementCompletions); return result; + + + void UpdateCompletions(string tagName, TagHelperDescriptor possibleDescriptor) + { + if (!elementCompletions.TryGetValue(tagName, out var existingRuleDescriptors)) + { + existingRuleDescriptors = new HashSet(); + elementCompletions[tagName] = existingRuleDescriptors; + } + + existingRuleDescriptors.Add(possibleDescriptor); + } } private void AddAllowedChildrenCompletions( diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperCompletionServiceTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperCompletionServiceTest.cs index fd01c2e5c6..0ec524fa09 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperCompletionServiceTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperCompletionServiceTest.cs @@ -10,6 +10,39 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor { public class DefaultTagHelperCompletionServiceTest { + [Fact] + public void GetElementCompletions_CatchAllsApplyToAllCompletions() + { + // Arrange + var documentDescriptors = new[] + { + TagHelperDescriptorBuilder.Create("SuperLiTagHelper", "TestAssembly") + .TagMatchingRule(rule => rule.RequireTagName("superli")) + .Build(), + TagHelperDescriptorBuilder.Create("CatchAll", "TestAssembly") + .TagMatchingRule(rule => rule.RequireTagName("*")) + .Build(), + }; + var expectedCompletions = ElementCompletionResult.Create(new Dictionary>() + { + ["superli"] = new HashSet { documentDescriptors[0], documentDescriptors[1] }, + ["li"] = new HashSet { documentDescriptors[1] }, + }); + + var existingCompletions = new[] { "li" }; + var completionContext = BuildCompletionContext( + documentDescriptors, + existingCompletions, + containingTagName: "ul"); + var service = CreateTagHelperCompletionFactsService(); + + // Act + var completions = service.GetElementCompletions(completionContext); + + // Assert + AssertCompletionsAreEquivalent(expectedCompletions, completions); + } + [Fact] public void GetElementCompletions_AllowsMultiTargetingTagHelpers() { @@ -183,13 +216,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor .AllowChildTag("div") .Build(), }; - var expectedCompletions = ElementCompletionResult.Create(new Dictionary>() - { - ["p"] = new HashSet(), - ["em"] = new HashSet(), - }); + var expectedCompletions = ElementCompletionResult.Create(new Dictionary>()); - var existingCompletions = new[] { "p", "em" }; + var existingCompletions = Enumerable.Empty(); var completionContext = BuildCompletionContext( documentDescriptors, existingCompletions,