Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,24 @@ namespace Microsoft.CodeAnalysis.Razor.Completion;
internal sealed class ElementCompletionResult
{
public IReadOnlyDictionary<string, IEnumerable<TagHelperDescriptor>> Completions { get; }
public IReadOnlyDictionary<string, IEnumerable<TagHelperDescriptor>> CompletionsWithUsing { get; }

private ElementCompletionResult(IReadOnlyDictionary<string, IEnumerable<TagHelperDescriptor>> completions)
private ElementCompletionResult(
IReadOnlyDictionary<string, IEnumerable<TagHelperDescriptor>> completions,
IReadOnlyDictionary<string, IEnumerable<TagHelperDescriptor>> completionsWithUsing)
{
Completions = completions;
CompletionsWithUsing = completionsWithUsing;
}

internal static ElementCompletionResult Create(Dictionary<string, HashSet<TagHelperDescriptor>> completions)
{
return Create(completions, new Dictionary<string, HashSet<TagHelperDescriptor>>());
}

internal static ElementCompletionResult Create(
Dictionary<string, HashSet<TagHelperDescriptor>> completions,
Dictionary<string, HashSet<TagHelperDescriptor>> completionsWithUsing)
{
if (completions is null)
{
Expand All @@ -32,6 +43,15 @@ internal static ElementCompletionResult Create(Dictionary<string, HashSet<TagHel
readonlyCompletions.Add(key, value);
}

return new ElementCompletionResult(readonlyCompletions);
var readonlyCompletionsWithUsing = new Dictionary<string, IEnumerable<TagHelperDescriptor>>(
capacity: completionsWithUsing.Count,
comparer: completionsWithUsing.Comparer);

foreach (var (key, value) in completionsWithUsing)
{
readonlyCompletionsWithUsing.Add(key, value);
}

return new ElementCompletionResult(readonlyCompletions, readonlyCompletionsWithUsing);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,10 @@ public static RazorCompletionItem CreateDirectiveAttributeEventParameterHtmlEven
string displayText, string insertText,
ImmutableArray<RazorCommitCharacter> commitCharacters)
=> new(RazorCompletionItemKind.DirectiveAttributeParameterEventValue, displayText, insertText, sortText: null, descriptionInfo: AggregateBoundAttributeDescription.Empty, commitCharacters, isSnippet: false);

public static RazorCompletionItem CreateTagHelperElementWithUsing(
string displayText, string insertText,
TagHelperElementWithUsingDescription descriptionInfo,
ImmutableArray<RazorCommitCharacter> commitCharacters)
=> new(RazorCompletionItemKind.TagHelperElementWithUsing, displayText, insertText, sortText: null, descriptionInfo, commitCharacters, isSnippet: false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ internal enum RazorCompletionItemKind
MarkupTransition,
TagHelperElement,
TagHelperAttribute,
TagHelperElementWithUsing,
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,28 @@ internal class RazorCompletionItemResolver : CompletionItemResolver
.ConfigureAwait(false);
}

break;
}
case RazorCompletionItemKind.TagHelperElementWithUsing:
{
if (associatedRazorCompletion.DescriptionInfo is not TagHelperElementWithUsingDescription descriptionInfo)
{
break;
}

if (useDescriptionProperty)
{
tagHelperClassifiedTextTooltip = await ClassifiedTagHelperTooltipFactory
.TryCreateTooltipAsync(razorCompletionResolveContext.FilePath, descriptionInfo.ElementDescription, componentAvailabilityService, cancellationToken)
.ConfigureAwait(false);
}
else
{
tagHelperMarkupTooltip = await MarkupTagHelperTooltipFactory
.TryCreateTooltipAsync(razorCompletionResolveContext.FilePath, descriptionInfo.ElementDescription, componentAvailabilityService, documentationKind, cancellationToken)
.ConfigureAwait(false);
}

break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,23 @@ internal static bool TryConvert(
completionItem = tagHelperAttributeCompletionItem;
return true;
}
case RazorCompletionItemKind.TagHelperElementWithUsing:
{
var tagHelperElementWithUsingCompletionItem = new VSInternalCompletionItem()
{
Label = razorCompletionItem.DisplayText,
InsertText = razorCompletionItem.InsertText,
FilterText = razorCompletionItem.InsertText,
SortText = razorCompletionItem.SortText,
InsertTextFormat = insertTextFormat,
Kind = tagHelperCompletionItemKind,
};

tagHelperElementWithUsingCompletionItem.UseCommitCharactersFrom(razorCompletionItem, clientCapabilities);

completionItem = tagHelperElementWithUsingCompletionItem;
return true;
}
}

completionItem = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,34 @@ private ImmutableArray<RazorCompletionItem> GetElementCompletions(
completionItems.Add(razorCompletionItem);
}

// Add completion items for fully qualified components that need @using statements
foreach (var (shortName, tagHelpers) in completionResult.CompletionsWithUsing)
{
foreach (var tagHelper in tagHelpers)
{
// Extract namespace from the fully qualified name
var lastDotIndex = tagHelper.Name.LastIndexOf('.');
if (lastDotIndex > 0)
{
var @namespace = tagHelper.Name[..lastDotIndex];
var displayText = $"{shortName} - @using {@namespace}";

var tagHelperDescriptions = ImmutableArray.Create(BoundElementDescriptionInfo.From(tagHelper));
var descriptionInfo = new TagHelperElementWithUsingDescription(
new(tagHelperDescriptions),
@namespace);

var razorCompletionItem = RazorCompletionItem.CreateTagHelperElementWithUsing(
displayText: displayText,
insertText: shortName,
descriptionInfo: descriptionInfo,
commitCharacters: commitChars);

completionItems.Add(razorCompletionItem);
}
}
}

return completionItems.ToImmutableAndClear();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ public ElementCompletionResult GetElementCompletions(ElementCompletionContext co
var catchAllDescriptors = new HashSet<TagHelperDescriptor>();
var prefix = completionContext.DocumentContext.Prefix ?? string.Empty;
var possibleChildDescriptors = TagHelperFacts.GetTagHelpersGivenParent(completionContext.DocumentContext, completionContext.ContainingParentTagName);
possibleChildDescriptors = FilterFullyQualifiedCompletions(possibleChildDescriptors);
var (filteredDescriptors, fullyQualifiedDescriptors) = FilterFullyQualifiedCompletionsWithTracking(possibleChildDescriptors);
possibleChildDescriptors = filteredDescriptors;
foreach (var possibleDescriptor in possibleChildDescriptors)
{
var addRuleCompletions = false;
Expand Down Expand Up @@ -250,7 +251,41 @@ public ElementCompletionResult GetElementCompletions(ElementCompletionContext co
}
}

var result = ElementCompletionResult.Create(elementCompletions);
// Process fully qualified descriptors that were filtered out to create "with using" completions
var completionsWithUsing = new Dictionary<string, HashSet<TagHelperDescriptor>>(StringComparer.Ordinal);
foreach (var fullyQualifiedDescriptor in fullyQualifiedDescriptors)
{
if (fullyQualifiedDescriptor.BoundAttributes.Any(static boundAttribute => boundAttribute.IsDirectiveAttribute))
{
// Skip directive attributes
continue;
}

foreach (var rule in fullyQualifiedDescriptor.TagMatchingRules)
{
if (rule.TagName == TagHelperMatchingConventions.ElementCatchAllName)
{
continue;
}

// Extract the short name from the fully qualified name
// e.g., "MyNamespace.MyComponent" -> "MyComponent"
var tagName = rule.TagName;
if (!string.IsNullOrEmpty(tagName) &&
TagHelperMatchingConventions.SatisfiesAttributes(rule, tagAttributes))
{
if (!completionsWithUsing.TryGetValue(tagName, out var descriptors))
{
descriptors = new HashSet<TagHelperDescriptor>();
completionsWithUsing[tagName] = descriptors;
}

descriptors.Add(fullyQualifiedDescriptor);
}
}
}

var result = ElementCompletionResult.Create(elementCompletions, completionsWithUsing);
return result;

static void UpdateCompletions(string tagName, TagHelperDescriptor possibleDescriptor, Dictionary<string, HashSet<TagHelperDescriptor>> elementCompletions, HashSet<TagHelperDescriptor>? tagHelperDescriptors = null)
Expand Down Expand Up @@ -334,7 +369,13 @@ private void AddAllowedChildrenCompletions(
}
}

private static ImmutableArray<TagHelperDescriptor> FilterFullyQualifiedCompletions(ImmutableArray<TagHelperDescriptor> possibleChildDescriptors)
internal static ImmutableArray<TagHelperDescriptor> FilterFullyQualifiedCompletions(ImmutableArray<TagHelperDescriptor> possibleChildDescriptors)
{
var (filteredDescriptors, _) = FilterFullyQualifiedCompletionsWithTracking(possibleChildDescriptors);
return filteredDescriptors;
}

private static (ImmutableArray<TagHelperDescriptor> FilteredDescriptors, ImmutableArray<TagHelperDescriptor> FilteredOutFullyQualified) FilterFullyQualifiedCompletionsWithTracking(ImmutableArray<TagHelperDescriptor> possibleChildDescriptors)
{
// Iterate once through the list to tease apart fully qualified and short name TagHelpers
using var fullyQualifiedTagHelpers = new PooledArrayBuilder<TagHelperDescriptor>();
Expand All @@ -355,6 +396,7 @@ private static ImmutableArray<TagHelperDescriptor> FilterFullyQualifiedCompletio
// Re-combine the short named & fully qualified TagHelpers but filter out any fully qualified TagHelpers that have a short
// named representation already.
using var filteredList = new PooledArrayBuilder<TagHelperDescriptor>(capacity: shortNameTagHelpers.Count);
using var filteredOutList = new PooledArrayBuilder<TagHelperDescriptor>();
filteredList.AddRange(shortNameTagHelpers);

foreach (var fullyQualifiedTagHelper in fullyQualifiedTagHelpers)
Expand All @@ -366,10 +408,12 @@ private static ImmutableArray<TagHelperDescriptor> FilterFullyQualifiedCompletio
}
else
{
// There's already a shortname variant of this item, don't include it.
// There's already a shortname variant of this item, don't include it in the main list.
// But we'll track it for creating "with using" completions.
filteredOutList.Add(fullyQualifiedTagHelper);
}
}

return filteredList.ToImmutableAndClear();
return (filteredList.ToImmutableAndClear(), filteredOutList.ToImmutableAndClear());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis.Razor.Tooltip;

namespace Microsoft.CodeAnalysis.Razor.Completion;

/// <summary>
/// Represents a completion item for a TagHelper element that requires adding a @using statement.
/// </summary>
internal sealed record TagHelperElementWithUsingDescription(
AggregateBoundElementDescription ElementDescription,
string Namespace)
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,19 @@ private async ValueTask<VSInternalCompletionItem> ResolveRazorCompletionItemAsyn
cancellationToken).ConfigureAwait(false);

// If we couldn't resolve, fall back to what we were passed in
return result ?? request;
result ??= request;

// Check if this is a TagHelperElementWithUsing completion and add the @using statement
var associatedRazorCompletion = razorResolutionContext.CompletionItems.FirstOrDefault(completion => completion.DisplayText == result.Label);
if (associatedRazorCompletion?.DescriptionInfo is TagHelperElementWithUsingDescription descriptionInfo)
{
var codeDocument = await context.Snapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false);
var addUsingEdit = AddUsingsHelper.CreateAddUsingTextEdit(descriptionInfo.Namespace, codeDocument);

result.AdditionalTextEdits = [addUsingEdit];
}

return result;
}

private async ValueTask<VSInternalCompletionItem> ResolveCSharpCompletionItemAsync(RemoteDocumentContext context, VSInternalCompletionItem request, VSInternalCompletionList containingCompletionList, DelegatedCompletionResolutionContext resolutionContext, RazorFormattingOptions formattingOptions, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,13 @@ The end.
htmlItemLabels: ["div", "h1"]);
}

// NOTE: Comprehensive testing for out-of-scope component completions with @using statements
// requires setting up a project with components in specific namespaces. The feature implementation
// is complete and will show completions like "ComponentName - @using Namespace" for components
// that are available in the project but not currently imported. When committed, these completions
// will automatically add the @using statement at the top of the file.
// See TagHelperCompletionProvider and TagHelperCompletionService for implementation details.

[Fact]
public async Task HtmlElementNamesAndTagHelpersCompletion_EndOfDocument()
{
Expand Down