Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
473db3f
Initial plan
Copilot Oct 13, 2025
3cfa493
Add snippet support for component completion with EditorRequired attr…
Copilot Oct 13, 2025
61fbb8e
Address PR feedback: use pooled StringBuilder, always add quotes, add…
Copilot Oct 13, 2025
a98a719
Simplify Cohost test and optimize component tag helper lookup
Copilot Oct 13, 2025
7272bb5
Merge remote-tracking branch 'origin/main' into copilot/add-completio…
Copilot Oct 14, 2025
58fa7d4
Fix test for snippet completion - snippets don't need resolve
Copilot Oct 15, 2025
0ba4329
Run activate.sh after restore to set DOTNET_ROOT etc.
davidwengier Oct 15, 2025
02b3f11
Update snippet display text and simplify test using helper
Copilot Oct 15, 2025
d701787
Update test infra to support InsertText and AdditionalEdits
davidwengier Oct 16, 2025
e396fda
Fix test
davidwengier Oct 16, 2025
c128f7d
Optimize to avoid .Any() and .ToImmutableArray() allocation
Copilot Oct 16, 2025
bac8a8b
Use localized resource string for component completion label
Copilot Oct 16, 2025
c5b5633
Use SR resource in test for component completion label
Copilot Oct 16, 2025
cb304fd
Merge branch 'main' into copilot/add-completion-snippet-for-editor-re…
davidwengier Oct 16, 2025
a4bda3d
Extract snippet completion logic to separate method
Copilot Oct 16, 2025
f20fdb8
Refactor: invert if for early exit and pass descriptionInfo from caller
Copilot Oct 16, 2025
1350ef3
Merge main into branch and resolve conflicts
Oct 17, 2025
5911e37
Add comment to SR.resx explaining "req'd" abbreviation
Copilot Oct 17, 2025
3b7914e
Update src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completi…
davidwengier Oct 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ public static RazorCompletionItem CreateTagHelperElement(
string displayText, string insertText,
AggregateBoundElementDescription descriptionInfo,
ImmutableArray<RazorCommitCharacter> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,24 @@ private ImmutableArray<RazorCompletionItem> 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);
}

Expand Down Expand Up @@ -307,6 +318,79 @@ private static ImmutableArray<RazorCommitCharacter> ResolveAttributeCommitCharac
};
}

private static void AddCompletionItemWithRequiredAttributesSnippet(
ref PooledArrayBuilder<RazorCompletionItem> completionItems,
RazorCompletionContext context,
IEnumerable<TagHelperDescriptor> tagHelpers,
string displayText,
AggregateBoundElementDescription descriptionInfo,
ImmutableArray<RazorCommitCharacter> 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<TagHelperDescriptor> 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</");
builder.Append(tagName);
builder.Append('>');

snippetText = builder.ToString();
return true;
}

private enum AttributeContext
{
Indexer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,8 @@
<data name="ExtractTo_Css_Title" xml:space="preserve">
<value>Extract to {0}.css</value>
</data>
<data name="ComponentCompletionWithRequiredAttributesLabel" xml:space="preserve">
<value>{0} (and req'd attributes...)</value>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot add a <comment> element under this that says The term "req'd" is an abbreviation for "required"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit. Added comment element explaining that "req'd" is an abbreviation for "required" to help with localization.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use an abbreviation in user visible text?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just thought the full word pushed things out to be too wide. Happy to follow up if there is better wording here (but selfishly, I want to get this merged so copilot stops complaining about firewall rules in other PRs 😛)

<comment>The term "req'd" is an abbreviation for "required"</comment>
</data>
</root>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading