diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 0ecdf5e3b89..f07e6cc7306 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -40,6 +40,11 @@ jobs: # Use restore script, but don't fail on errors so Copilot can still attempt to work run: ./restore.sh + # Activate the private .NET install. Hopefully this resolves firewall issues when using dotnet build/test + - name: Activate + continue-on-error: true + run: source ./activate.sh + # Diagnostics in the log - name: Show .NET info run: dotnet --info diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs index e6a4528b08c..8e4717f15ff 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs @@ -87,8 +87,9 @@ public static RazorCompletionItem CreateTagHelperElement( string displayText, string insertText, AggregateBoundElementDescription descriptionInfo, ImmutableArray commitCharacters, + bool isSnippet = false, TextEdit[]? additionalTextEdits = null) - => new(RazorCompletionItemKind.TagHelperElement, displayText, insertText, sortText: null, descriptionInfo, commitCharacters, isSnippet: false, additionalTextEdits); + => new(RazorCompletionItemKind.TagHelperElement, displayText, insertText, sortText: null, descriptionInfo, commitCharacters, isSnippet, additionalTextEdits); public static RazorCompletionItem CreateTagHelperAttribute( string displayText, string insertText, string? sortText, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionProvider.cs index c8fb0b9e6f9..11ec80c3498 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionProvider.cs @@ -231,13 +231,24 @@ private ImmutableArray GetElementCompletions( { var descriptionInfo = new AggregateBoundElementDescription(tagHelpers.SelectAsArray(BoundElementDescriptionInfo.From)); + // Always add the regular completion item var razorCompletionItem = RazorCompletionItem.CreateTagHelperElement( displayText: displayText, insertText: displayText, descriptionInfo, - commitCharacters: commitChars); + commitCharacters: commitChars, + isSnippet: false); completionItems.Add(razorCompletionItem); + + AddCompletionItemWithRequiredAttributesSnippet( + ref completionItems.AsRef(), + context, + tagHelpers, + displayText, + descriptionInfo, + commitChars); + AddCompletionItemWithUsingDirective(ref completionItems.AsRef(), context, commitChars, displayText, descriptionInfo); } @@ -307,6 +318,79 @@ private static ImmutableArray ResolveAttributeCommitCharac }; } + private static void AddCompletionItemWithRequiredAttributesSnippet( + ref PooledArrayBuilder completionItems, + RazorCompletionContext context, + IEnumerable tagHelpers, + string displayText, + AggregateBoundElementDescription descriptionInfo, + ImmutableArray commitChars) + { + // If snippets are not supported, exit early + if (!context.Options.SnippetsSupported) + { + return; + } + + if (TryGetEditorRequiredAttributesSnippet(tagHelpers, displayText, out var snippetText)) + { + var snippetCompletionItem = RazorCompletionItem.CreateTagHelperElement( + displayText: SR.FormatComponentCompletionWithRequiredAttributesLabel(displayText), + insertText: snippetText, + descriptionInfo: descriptionInfo, + commitCharacters: commitChars, + isSnippet: true); + + completionItems.Add(snippetCompletionItem); + } + } + + private static bool TryGetEditorRequiredAttributesSnippet( + IEnumerable tagHelpers, + string tagName, + [NotNullWhen(true)] out string? snippetText) + { + // For components, there should only be one tag helper descriptor per component name + // Get EditorRequired attributes from the first component tag helper + var componentTagHelper = tagHelpers.FirstOrDefault(th => th.Kind == TagHelperKind.Component); + if (componentTagHelper is null) + { + snippetText = null; + return false; + } + + var requiredAttributes = componentTagHelper.EditorRequiredAttributes; + if (requiredAttributes.Length == 0) + { + snippetText = null; + return false; + } + + // Build snippet with placeholders for each required attribute + using var _ = StringBuilderPool.GetPooledObject(out var builder); + builder.Append(tagName); + + var tabStopIndex = 1; + foreach (var attribute in requiredAttributes) + { + builder.Append(' '); + builder.Append(attribute.Name); + builder.Append("=\"$"); + builder.Append(tabStopIndex); + builder.Append('"'); + + tabStopIndex++; + } + + // Add final tab stop for the element content + builder.Append(">$0'); + + snippetText = builder.ToString(); + return true; + } + private enum AttributeContext { Indexer, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx index f406690a6e9..438bbef9c15 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx @@ -223,4 +223,8 @@ Extract to {0}.css + + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.cs.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.cs.xlf index 42df5a9025d..79d83e0655d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.cs.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.cs.xlf @@ -7,6 +7,11 @@ Hodnota nesmí být null ani prázdný řetězec. + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag Vytvořit komponentu ze značky diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.de.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.de.xlf index c393cee508a..87c57d42298 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.de.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.de.xlf @@ -7,6 +7,11 @@ Der Wert darf nicht NULL oder eine leere Zeichenfolge sein. + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag Komponente aus Tag erstellen diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.es.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.es.xlf index b6025490d1d..04a660d3200 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.es.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.es.xlf @@ -7,6 +7,11 @@ El valor no puede ser nulo ni una cadena vacía. + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag Crear un componente a partir de la etiqueta diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.fr.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.fr.xlf index ef64355555b..c345ebd7c39 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.fr.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.fr.xlf @@ -7,6 +7,11 @@ La valeur ne peut pas être Null ni être une chaîne vide. + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag Créer un composant à partir de la balise diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.it.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.it.xlf index 09d944da93e..7f922bc2451 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.it.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.it.xlf @@ -7,6 +7,11 @@ Il valore non può essere null o una stringa vuota. + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag Crea componente da tag diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ja.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ja.xlf index 96bec9b48b2..edcf8dc045c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ja.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ja.xlf @@ -7,6 +7,11 @@ 値を null または空の文字列にすることはできません。 + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag タグからコンポーネントを作成する diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ko.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ko.xlf index 1e42f2e9510..a9056fe7d1d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ko.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ko.xlf @@ -7,6 +7,11 @@ 값은 null이거나 빈 문자열일 수 없습니다. + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag 태그에서 구성 요소 만들기 diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pl.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pl.xlf index 1a9711bfde7..e5b1b85dbfa 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pl.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pl.xlf @@ -7,6 +7,11 @@ Wartość nie może być wartością null ani pustym ciągiem. + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag Utwórz składnik z tagu diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pt-BR.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pt-BR.xlf index fee4ff24f59..59589c25e07 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pt-BR.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pt-BR.xlf @@ -7,6 +7,11 @@ O valor não pode ser nulo ou uma cadeia de caracteres vazia. + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag Criar componente a partir da marca diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ru.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ru.xlf index f0b27b0b57a..f9e6ce4cf18 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ru.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ru.xlf @@ -7,6 +7,11 @@ Значение не может быть NULL или пустой строкой. + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag Создание компонента из тега diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.tr.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.tr.xlf index 69a944d2c51..276ab0d3036 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.tr.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.tr.xlf @@ -7,6 +7,11 @@ Değer null veya boş bir dize olamaz. + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag Etiketten bileşen oluştur diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hans.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hans.xlf index 45b2ec594d2..9ba505c21b1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hans.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hans.xlf @@ -7,6 +7,11 @@ 值不能为 null 或空字符串。 + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag 从标记创建组件 diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hant.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hant.xlf index dccee2767ce..adec78cbe17 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hant.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hant.xlf @@ -7,6 +7,11 @@ 值不能為 Null 或空字串。 + + {0} (and req'd attributes...) + {0} (and req'd attributes...) + The term "req'd" is an abbreviation for "required" + Create component from tag 從標籤建立元件 diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperCompletionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperCompletionProviderTest.cs index fb9c0145584..29cc3fc059b 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperCompletionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperCompletionProviderTest.cs @@ -931,6 +931,161 @@ private static void AssertTest1Test2Completions(IReadOnlyList rule.TagName = "ComponentWithRequiredParams"); + componentBuilder.IsFullyQualifiedNameMatch = true; + componentBuilder.BindAttribute(attribute => + { + attribute.Name = "RequiredParam1"; + attribute.PropertyName = "RequiredParam1"; + attribute.TypeName = typeof(string).FullName; + attribute.IsEditorRequired = true; + }); + componentBuilder.BindAttribute(attribute => + { + attribute.Name = "RequiredParam2"; + attribute.PropertyName = "RequiredParam2"; + attribute.TypeName = typeof(int).FullName; + attribute.IsEditorRequired = true; + }); + componentBuilder.BindAttribute(attribute => + { + attribute.Name = "OptionalParam"; + attribute.PropertyName = "OptionalParam"; + attribute.TypeName = typeof(string).FullName; + }); + + var service = CreateTagHelperCompletionProvider(); + var options = new RazorCompletionOptions(SnippetsSupported: true, AutoInsertAttributeQuotes: true, CommitElementsWithSpace: true, UseVsCodeCompletionCommitCharacters: false); + var context = CreateRazorCompletionContext( + """ + <$$ + """, + isRazorFile: true, + options, + tagHelpers: ImmutableArray.Create(componentBuilder.Build())); + + // Act + var completions = service.GetCompletionItems(context); + + // Assert - should have two completions: regular and snippet + Assert.Equal(2, completions.Length); + + RazorCompletionItem regularCompletion = null; + RazorCompletionItem snippetCompletion = null; + + foreach (var completion in completions) + { + if (completion.DisplayText == "ComponentWithRequiredParams") + { + regularCompletion = completion; + } + else if (completion.DisplayText == "ComponentWithRequiredParams (and req'd attributes...)") + { + snippetCompletion = completion; + } + } + + Assert.NotNull(regularCompletion); + Assert.False(regularCompletion.IsSnippet); + Assert.Equal("ComponentWithRequiredParams", regularCompletion.InsertText); + + Assert.NotNull(snippetCompletion); + Assert.True(snippetCompletion.IsSnippet); + Assert.Contains("RequiredParam1", snippetCompletion.InsertText); + Assert.Contains("RequiredParam2", snippetCompletion.InsertText); + Assert.Contains("$1", snippetCompletion.InsertText); + Assert.Contains("$2", snippetCompletion.InsertText); + Assert.Contains("$0", snippetCompletion.InsertText); + // Verify quotes are always added in snippets + Assert.Contains("=\"$1\"", snippetCompletion.InsertText); + } + + [Fact] + public void GetCompletionAt_ComponentWithEditorRequiredAttributes_SnippetsNotSupported_ReturnsNonSnippet() + { + // Arrange + var componentBuilder = TagHelperDescriptorBuilder.CreateComponent("ComponentWithRequiredParams", "TestAssembly"); + componentBuilder.SetTypeName( + fullName: "TestNamespace.ComponentWithRequiredParams", + typeNamespace: "TestNamespace", + typeNameIdentifier: "ComponentWithRequiredParams"); + componentBuilder.TagMatchingRule(rule => rule.TagName = "ComponentWithRequiredParams"); + componentBuilder.IsFullyQualifiedNameMatch = true; + componentBuilder.BindAttribute(attribute => + { + attribute.Name = "RequiredParam1"; + attribute.PropertyName = "RequiredParam1"; + attribute.TypeName = typeof(string).FullName; + attribute.IsEditorRequired = true; + }); + + var service = CreateTagHelperCompletionProvider(); + var options = new RazorCompletionOptions(SnippetsSupported: false, AutoInsertAttributeQuotes: true, CommitElementsWithSpace: true, UseVsCodeCompletionCommitCharacters: false); + var context = CreateRazorCompletionContext( + """ + <$$ + """, + isRazorFile: true, + options, + tagHelpers: ImmutableArray.Create(componentBuilder.Build())); + + // Act + var completions = service.GetCompletionItems(context); + + // Assert + var completion = Assert.Single(completions); + Assert.Equal("ComponentWithRequiredParams", completion.DisplayText); + Assert.False(completion.IsSnippet); + Assert.Equal("ComponentWithRequiredParams", completion.InsertText); + } + + [Fact] + public void GetCompletionAt_ComponentWithNoEditorRequiredAttributes_SnippetsSupported_ReturnsNonSnippet() + { + // Arrange + var componentBuilder = TagHelperDescriptorBuilder.CreateComponent("ComponentWithoutRequired", "TestAssembly"); + componentBuilder.SetTypeName( + fullName: "TestNamespace.ComponentWithoutRequired", + typeNamespace: "TestNamespace", + typeNameIdentifier: "ComponentWithoutRequired"); + componentBuilder.TagMatchingRule(rule => rule.TagName = "ComponentWithoutRequired"); + componentBuilder.IsFullyQualifiedNameMatch = true; + componentBuilder.BindAttribute(attribute => + { + attribute.Name = "OptionalParam"; + attribute.PropertyName = "OptionalParam"; + attribute.TypeName = typeof(string).FullName; + }); + + var service = CreateTagHelperCompletionProvider(); + var options = new RazorCompletionOptions(SnippetsSupported: true, AutoInsertAttributeQuotes: true, CommitElementsWithSpace: true, UseVsCodeCompletionCommitCharacters: false); + var context = CreateRazorCompletionContext( + """ + <$$ + """, + isRazorFile: true, + options, + tagHelpers: ImmutableArray.Create(componentBuilder.Build())); + + // Act + var completions = service.GetCompletionItems(context); + + // Assert + var completion = Assert.Single(completions); + Assert.Equal("ComponentWithoutRequired", completion.DisplayText); + Assert.False(completion.IsSnippet); + Assert.Equal("ComponentWithoutRequired", completion.InsertText); + } + private static RazorCompletionContext CreateRazorCompletionContext(string markup, bool isRazorFile, RazorCompletionOptions options = default, ImmutableArray tagHelpers = default) { tagHelpers = tagHelpers.NullToEmpty(); diff --git a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostDocumentCompletionEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostDocumentCompletionEndpointTest.cs index b7fc275b796..2d29f65b8ca 100644 --- a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostDocumentCompletionEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostDocumentCompletionEndpointTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -19,6 +18,7 @@ using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Settings; using Microsoft.CodeAnalysis.Razor.Telemetry; +using Microsoft.CodeAnalysis.Razor.Workspaces.Resources; using Roslyn.Test.Utilities; using Roslyn.Text.Adornments; using Xunit; @@ -810,6 +810,36 @@ The end. Assert.All(list.Items, item => Assert.DoesNotContain("=", item.CommitCharacters ?? [])); } + [Fact] + public async Task ComponentWithEditorRequiredAttributes() + { + await VerifyCompletionListAsync( + input: """ + This is a Razor document. + + <$$ + + The end. + """, + completionContext: new VSInternalCompletionContext() + { + InvokeKind = VSInternalCompletionInvokeKind.Typing, + TriggerCharacter = "<", + TriggerKind = CompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["LayoutView", "EditForm", "ValidationMessage", "div", "Router", SR.FormatComponentCompletionWithRequiredAttributesLabel("Router")], + htmlItemLabels: ["div"], + itemToResolve: SR.FormatComponentCompletionWithRequiredAttributesLabel("Router"), + expectedResolvedItemDescription: "Microsoft.AspNetCore.Components.Routing.Router", + expected: $""" + This is a Razor document. + + $0 + + The end. + """); + } + [Fact] [WorkItem("https://github.com/dotnet/razor/issues/9378")] public async Task BlazorDataEnhanceAttributeCompletion_OnFormElement()