Skip to content

Commit c87ceed

Browse files
authored
Add snippet support for component completion with EditorRequired attributes (#12325)
- [x] Understand the current completion system and how TagHelper element completion works - [x] Modify RazorCompletionItem.CreateTagHelperElement to support snippet mode - [x] Update GetElementCompletions in TagHelperCompletionProvider to detect EditorRequired attributes - [x] Build snippet text with placeholders for EditorRequired properties - [x] Add comprehensive tests for the new snippet functionality - [x] Validate the changes work correctly - [x] Address PR feedback: - Changed to add a separate completion item for snippets (with "..." suffix) instead of modifying the existing item - Always add quotes in snippets (removed autoInsertAttributeQuotes parameter) - Use pooled StringBuilder instead of regular StringBuilder - Added test in CohostDocumentCompletionEndpointTest.cs - Simplified Cohost test to use VerifyCompletionListAsync helper - Optimized component tag helper lookup to use first Component kind tag helper only - [x] Merged main into branch (test files moved to VS Code project) - [x] Fixed test failure - snippet items don't need resolve, they already have complete insertText - [x] Updated snippet display text to "{displayText} (and req'd attributes...)" for better clarity - [x] Simplified test to use VerifyCompletionListAsync helper with verifySnippetItem callback - [x] Optimized to avoid .Any() and .ToImmutableArray() allocation by changing method to accept IEnumerable - [x] Added localized resource string for component completion label - [x] Updated tests to use SR resource for consistent labeling - [x] Extracted snippet completion logic to AddCompletionItemWithRequiredAttributesSnippet method - [x] Inverted if statement for early exit and pass descriptionInfo from calling method - [x] Merged latest main and resolved conflicts - both ComponentWithEditorRequiredAttributes and new Blazor data attribute tests now coexist - [x] Added comment to SR.resx explaining "req'd" abbreviation for localization <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Component completion snippet</issue_title> > <issue_description>Noticed today watching someone use the Razor editor: For components that have one or more properties that are `[EditorRequired]`, we could do completion as a snippet, that includes the attributes for each required property (similar to how drag and drop will create attributes for them) and either puts the caret in the first one, or even puts placeholders in each and lets the user tab between them.</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> Fixes #6980 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey3.medallia.com/?EAHeSx-AP01bZqG0Ld9QLQ) to start the survey.
2 parents 131c2fd + 3b7914e commit c87ceed

File tree

19 files changed

+347
-3
lines changed

19 files changed

+347
-3
lines changed

.github/workflows/copilot-setup-steps.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ jobs:
4040
# Use restore script, but don't fail on errors so Copilot can still attempt to work
4141
run: ./restore.sh
4242

43+
# Activate the private .NET install. Hopefully this resolves firewall issues when using dotnet build/test
44+
- name: Activate
45+
continue-on-error: true
46+
run: source ./activate.sh
47+
4348
# Diagnostics in the log
4449
- name: Show .NET info
4550
run: dotnet --info

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ public static RazorCompletionItem CreateTagHelperElement(
8787
string displayText, string insertText,
8888
AggregateBoundElementDescription descriptionInfo,
8989
ImmutableArray<RazorCommitCharacter> commitCharacters,
90+
bool isSnippet = false,
9091
TextEdit[]? additionalTextEdits = null)
91-
=> new(RazorCompletionItemKind.TagHelperElement, displayText, insertText, sortText: null, descriptionInfo, commitCharacters, isSnippet: false, additionalTextEdits);
92+
=> new(RazorCompletionItemKind.TagHelperElement, displayText, insertText, sortText: null, descriptionInfo, commitCharacters, isSnippet, additionalTextEdits);
9293

9394
public static RazorCompletionItem CreateTagHelperAttribute(
9495
string displayText, string insertText, string? sortText,

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionProvider.cs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,24 @@ private ImmutableArray<RazorCompletionItem> GetElementCompletions(
231231
{
232232
var descriptionInfo = new AggregateBoundElementDescription(tagHelpers.SelectAsArray(BoundElementDescriptionInfo.From));
233233

234+
// Always add the regular completion item
234235
var razorCompletionItem = RazorCompletionItem.CreateTagHelperElement(
235236
displayText: displayText,
236237
insertText: displayText,
237238
descriptionInfo,
238-
commitCharacters: commitChars);
239+
commitCharacters: commitChars,
240+
isSnippet: false);
239241

240242
completionItems.Add(razorCompletionItem);
243+
244+
AddCompletionItemWithRequiredAttributesSnippet(
245+
ref completionItems.AsRef(),
246+
context,
247+
tagHelpers,
248+
displayText,
249+
descriptionInfo,
250+
commitChars);
251+
241252
AddCompletionItemWithUsingDirective(ref completionItems.AsRef(), context, commitChars, displayText, descriptionInfo);
242253
}
243254

@@ -307,6 +318,79 @@ private static ImmutableArray<RazorCommitCharacter> ResolveAttributeCommitCharac
307318
};
308319
}
309320

321+
private static void AddCompletionItemWithRequiredAttributesSnippet(
322+
ref PooledArrayBuilder<RazorCompletionItem> completionItems,
323+
RazorCompletionContext context,
324+
IEnumerable<TagHelperDescriptor> tagHelpers,
325+
string displayText,
326+
AggregateBoundElementDescription descriptionInfo,
327+
ImmutableArray<RazorCommitCharacter> commitChars)
328+
{
329+
// If snippets are not supported, exit early
330+
if (!context.Options.SnippetsSupported)
331+
{
332+
return;
333+
}
334+
335+
if (TryGetEditorRequiredAttributesSnippet(tagHelpers, displayText, out var snippetText))
336+
{
337+
var snippetCompletionItem = RazorCompletionItem.CreateTagHelperElement(
338+
displayText: SR.FormatComponentCompletionWithRequiredAttributesLabel(displayText),
339+
insertText: snippetText,
340+
descriptionInfo: descriptionInfo,
341+
commitCharacters: commitChars,
342+
isSnippet: true);
343+
344+
completionItems.Add(snippetCompletionItem);
345+
}
346+
}
347+
348+
private static bool TryGetEditorRequiredAttributesSnippet(
349+
IEnumerable<TagHelperDescriptor> tagHelpers,
350+
string tagName,
351+
[NotNullWhen(true)] out string? snippetText)
352+
{
353+
// For components, there should only be one tag helper descriptor per component name
354+
// Get EditorRequired attributes from the first component tag helper
355+
var componentTagHelper = tagHelpers.FirstOrDefault(th => th.Kind == TagHelperKind.Component);
356+
if (componentTagHelper is null)
357+
{
358+
snippetText = null;
359+
return false;
360+
}
361+
362+
var requiredAttributes = componentTagHelper.EditorRequiredAttributes;
363+
if (requiredAttributes.Length == 0)
364+
{
365+
snippetText = null;
366+
return false;
367+
}
368+
369+
// Build snippet with placeholders for each required attribute
370+
using var _ = StringBuilderPool.GetPooledObject(out var builder);
371+
builder.Append(tagName);
372+
373+
var tabStopIndex = 1;
374+
foreach (var attribute in requiredAttributes)
375+
{
376+
builder.Append(' ');
377+
builder.Append(attribute.Name);
378+
builder.Append("=\"$");
379+
builder.Append(tabStopIndex);
380+
builder.Append('"');
381+
382+
tabStopIndex++;
383+
}
384+
385+
// Add final tab stop for the element content
386+
builder.Append(">$0</");
387+
builder.Append(tagName);
388+
builder.Append('>');
389+
390+
snippetText = builder.ToString();
391+
return true;
392+
}
393+
310394
private enum AttributeContext
311395
{
312396
Indexer,

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,8 @@
223223
<data name="ExtractTo_Css_Title" xml:space="preserve">
224224
<value>Extract to {0}.css</value>
225225
</data>
226+
<data name="ComponentCompletionWithRequiredAttributesLabel" xml:space="preserve">
227+
<value>{0} (and req'd attributes...)</value>
228+
<comment>The term "req'd" is an abbreviation for "required"</comment>
229+
</data>
226230
</root>

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.cs.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.de.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.es.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.fr.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.it.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ja.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)