diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Serialization/CompletionListSerializationBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Serialization/CompletionListSerializationBenchmark.cs index 1ab03171622..07f643b524d 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Serialization/CompletionListSerializationBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Serialization/CompletionListSerializationBenchmark.cs @@ -67,11 +67,12 @@ public void ComponentElement_CompletionList_Deserialization() private CompletionList GenerateCompletionList(string documentContent, int queryIndex, TagHelperCompletionProvider componentCompletionProvider) { var sourceDocument = RazorSourceDocument.Create(documentContent, RazorSourceDocumentProperties.Default); + var codeDocument = RazorCodeDocument.Create(sourceDocument); var syntaxTree = RazorSyntaxTree.Parse(sourceDocument); var tagHelperDocumentContext = TagHelperDocumentContext.Create(prefix: string.Empty, CommonResources.LegacyTagHelpers); var owner = syntaxTree.Root.FindInnermostNode(queryIndex, includeWhitespace: true, walkMarkersBack: true); - var context = new RazorCompletionContext(queryIndex, owner, syntaxTree, tagHelperDocumentContext); + var context = new RazorCompletionContext(codeDocument, queryIndex, owner, syntaxTree, tagHelperDocumentContext); var razorCompletionItems = componentCompletionProvider.GetCompletionItems(context); var completionList = RazorCompletionListProvider.CreateLSPCompletionList( diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionContext.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionContext.cs index 36b534ec19c..cc8527dfc34 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionContext.cs @@ -8,6 +8,7 @@ namespace Microsoft.CodeAnalysis.Razor.Completion; internal record RazorCompletionContext( + RazorCodeDocument CodeDocument, int AbsoluteIndex, RazorSyntaxNode? Owner, RazorSyntaxTree SyntaxTree, 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 e963ace3405..8252dae9a70 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItem.cs @@ -22,6 +22,7 @@ internal sealed class RazorCompletionItem public object DescriptionInfo { get; } public ImmutableArray CommitCharacters { get; } public bool IsSnippet { get; } + public TextEdit[]? AdditionalTextEdits { get; } /// /// Creates a new Razor completion item @@ -33,6 +34,7 @@ internal sealed class RazorCompletionItem /// An object that provides description information for this completion item. /// Characters that can be used to commit the completion item. /// Indicates whether the completion item's is an LSP snippet or not. + /// Additional text edits to apply when the completion is committed. /// Thrown if or are . private RazorCompletionItem( RazorCompletionItemKind kind, @@ -41,7 +43,8 @@ private RazorCompletionItem( string? sortText, object descriptionInfo, ImmutableArray commitCharacters, - bool isSnippet) + bool isSnippet, + TextEdit[]? additionalTextEdits = null) { ArgHelper.ThrowIfNull(displayText); ArgHelper.ThrowIfNull(insertText); @@ -53,6 +56,7 @@ private RazorCompletionItem( DescriptionInfo = descriptionInfo; CommitCharacters = commitCharacters.NullToEmpty(); IsSnippet = isSnippet; + AdditionalTextEdits = additionalTextEdits; } public static RazorCompletionItem CreateDirective( @@ -82,8 +86,9 @@ public static RazorCompletionItem CreateMarkupTransition( public static RazorCompletionItem CreateTagHelperElement( string displayText, string insertText, AggregateBoundElementDescription descriptionInfo, - ImmutableArray commitCharacters) - => new(RazorCompletionItemKind.TagHelperElement, displayText, insertText, sortText: null, descriptionInfo, commitCharacters, isSnippet: false); + ImmutableArray commitCharacters, + TextEdit[]? additionalTextEdits = null) + => new(RazorCompletionItemKind.TagHelperElement, displayText, insertText, sortText: null, descriptionInfo, commitCharacters, isSnippet: false, additionalTextEdits); public static RazorCompletionItem CreateTagHelperAttribute( string displayText, string insertText, string? sortText, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionListProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionListProvider.cs index 645d19e82bc..07510b2eeb4 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionListProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionListProvider.cs @@ -52,6 +52,7 @@ internal class RazorCompletionListProvider( owner = AbstractRazorCompletionFactsService.AdjustSyntaxNodeForWordBoundary(owner, absoluteIndex); var razorCompletionContext = new RazorCompletionContext( + codeDocument, absoluteIndex, owner, syntaxTree, @@ -136,6 +137,7 @@ internal static bool TryConvert( SortText = razorCompletionItem.SortText, InsertTextFormat = insertTextFormat, Kind = razorCompletionItem.IsSnippet ? CompletionItemKind.Snippet : CompletionItemKind.Keyword, + AdditionalTextEdits = razorCompletionItem.AdditionalTextEdits, }; directiveCompletionItem.UseCommitCharactersFrom(razorCompletionItem, clientCapabilities); @@ -159,6 +161,7 @@ internal static bool TryConvert( SortText = razorCompletionItem.SortText, InsertTextFormat = insertTextFormat, Kind = tagHelperCompletionItemKind, + AdditionalTextEdits = razorCompletionItem.AdditionalTextEdits, }; directiveAttributeCompletionItem.UseCommitCharactersFrom(razorCompletionItem, clientCapabilities); @@ -176,6 +179,7 @@ internal static bool TryConvert( SortText = razorCompletionItem.SortText, InsertTextFormat = insertTextFormat, Kind = tagHelperCompletionItemKind, + AdditionalTextEdits = razorCompletionItem.AdditionalTextEdits, }; parameterCompletionItem.UseCommitCharactersFrom(razorCompletionItem, clientCapabilities); @@ -193,6 +197,7 @@ internal static bool TryConvert( SortText = razorCompletionItem.SortText, InsertTextFormat = insertTextFormat, Kind = CompletionItemKind.Event, + AdditionalTextEdits = razorCompletionItem.AdditionalTextEdits, }; eventValueCompletionItem.UseCommitCharactersFrom(razorCompletionItem, clientCapabilities); @@ -210,6 +215,7 @@ internal static bool TryConvert( SortText = razorCompletionItem.SortText, InsertTextFormat = insertTextFormat, Kind = tagHelperCompletionItemKind, + AdditionalTextEdits = razorCompletionItem.AdditionalTextEdits, }; markupTransitionCompletionItem.UseCommitCharactersFrom(razorCompletionItem, clientCapabilities); @@ -227,6 +233,7 @@ internal static bool TryConvert( SortText = razorCompletionItem.SortText, InsertTextFormat = insertTextFormat, Kind = tagHelperCompletionItemKind, + AdditionalTextEdits = razorCompletionItem.AdditionalTextEdits, }; tagHelperElementCompletionItem.UseCommitCharactersFrom(razorCompletionItem, clientCapabilities); @@ -244,6 +251,7 @@ internal static bool TryConvert( SortText = razorCompletionItem.SortText, InsertTextFormat = insertTextFormat, Kind = tagHelperCompletionItemKind, + AdditionalTextEdits = razorCompletionItem.AdditionalTextEdits, }; tagHelperAttributeCompletionItem.UseCommitCharactersFrom(razorCompletionItem, clientCapabilities); 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 f032772b89a..b5d3e7acb43 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionProvider.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Tooltip; using Microsoft.VisualStudio.Editor.Razor; using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; @@ -233,20 +234,48 @@ private ImmutableArray GetElementCompletions( foreach (var (displayText, tagHelpers) in completionResult.Completions) { - var tagHelperDescriptions = tagHelpers.SelectAsArray(BoundElementDescriptionInfo.From); + var descriptionInfo = new AggregateBoundElementDescription(tagHelpers.SelectAsArray(BoundElementDescriptionInfo.From)); var razorCompletionItem = RazorCompletionItem.CreateTagHelperElement( displayText: displayText, insertText: displayText, - descriptionInfo: new(tagHelperDescriptions), + descriptionInfo, commitCharacters: commitChars); completionItems.Add(razorCompletionItem); + AddCompletionItemWithUsingDirective(ref completionItems.AsRef(), context, commitChars, displayText, descriptionInfo); } return completionItems.ToImmutableAndClear(); } + private static void AddCompletionItemWithUsingDirective(ref PooledArrayBuilder completionItems, RazorCompletionContext context, ImmutableArray commitChars, string displayText, AggregateBoundElementDescription descriptionInfo) + { + // If this is a fully qualified name (contains a dot), it means there's an out-of-scope component + // so we add an additional completion item with @using hint and additional edits that will insert + // the @using correctly. + var lastDotIndex = displayText.LastIndexOf('.'); + if (lastDotIndex == -1) + { + return; + } + + var @namespace = displayText[..lastDotIndex]; + var shortName = displayText[(lastDotIndex + 1)..]; // Get the short name after the last dot + var displayTextWithUsing = $"{shortName} - @using {@namespace}"; + + var addUsingEdit = AddUsingsHelper.CreateAddUsingTextEdit(@namespace, context.CodeDocument); + + var razorCompletionItemWithUsing = RazorCompletionItem.CreateTagHelperElement( + displayText: displayTextWithUsing, + insertText: shortName, + descriptionInfo, + commitCharacters: commitChars, + additionalTextEdits: [addUsingEdit]); + + completionItems.Add(razorCompletionItemWithUsing); + } + private const string BooleanTypeString = "System.Boolean"; private static AttributeContext ResolveAttributeContext( diff --git a/src/Razor/src/Microsoft.VisualStudio.LegacyEditor.Razor/Completion/RazorDirectiveAttributeCompletionSource.cs b/src/Razor/src/Microsoft.VisualStudio.LegacyEditor.Razor/Completion/RazorDirectiveAttributeCompletionSource.cs index 6a84319e742..4f579d377ef 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LegacyEditor.Razor/Completion/RazorDirectiveAttributeCompletionSource.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LegacyEditor.Razor/Completion/RazorDirectiveAttributeCompletionSource.cs @@ -78,7 +78,7 @@ public async Task GetCompletionContextAsync(IAsyncCompletionS #pragma warning disable CS0618 // Type or member is obsolete, will be removed in an upcoming change var owner = syntaxTree.Root.LocateOwner(queryableChange); #pragma warning restore CS0618 // Type or member is obsolete - var razorCompletionContext = new RazorCompletionContext(absoluteIndex, owner, syntaxTree, tagHelperContext); + var razorCompletionContext = new RazorCompletionContext(codeDocument, absoluteIndex, owner, syntaxTree, tagHelperContext); var razorCompletionItems = _completionFactsService.GetCompletionItems(razorCompletionContext); if (razorCompletionItems.Length == 0) diff --git a/src/Razor/src/Microsoft.VisualStudio.LegacyEditor.Razor/Completion/RazorDirectiveCompletionSource.cs b/src/Razor/src/Microsoft.VisualStudio.LegacyEditor.Razor/Completion/RazorDirectiveCompletionSource.cs index f795aae98b4..cf790c26b2e 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LegacyEditor.Razor/Completion/RazorDirectiveCompletionSource.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LegacyEditor.Razor/Completion/RazorDirectiveCompletionSource.cs @@ -80,7 +80,7 @@ public async Task GetCompletionContextAsync( #pragma warning disable CS0618 // Type or member is obsolete, will be removed in an upcoming change var owner = syntaxTree.Root.LocateOwner(queryableChange); #pragma warning restore CS0618 // Type or member is obsolete - var razorCompletionContext = new RazorCompletionContext(absoluteIndex, owner, syntaxTree, tagHelperContext); + var razorCompletionContext = new RazorCompletionContext(codeDocument, absoluteIndex, owner, syntaxTree, tagHelperContext); var razorCompletionItems = _completionFactsService.GetCompletionItems(razorCompletionContext); using var _ = ArrayBuilderPool.GetPooledObject(out var completionItems); 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 5226b0923ae..fb9c0145584 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 @@ -942,6 +942,6 @@ private static RazorCompletionContext CreateRazorCompletionContext(string markup var owner = syntaxTree.Root.FindInnermostNode(position, includeWhitespace: true, walkMarkersBack: true); owner = AbstractRazorCompletionFactsService.AdjustSyntaxNodeForWordBoundary(owner, position); - return new RazorCompletionContext(position, owner, syntaxTree, context, Options: options); + return new RazorCompletionContext(codeDocument, position, owner, syntaxTree, context, Options: options); } } diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DefaultRazorCompletionFactsServiceTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DefaultRazorCompletionFactsServiceTest.cs index 83f104a9b1a..883f42a2227 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DefaultRazorCompletionFactsServiceTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DefaultRazorCompletionFactsServiceTest.cs @@ -15,6 +15,8 @@ public class DefaultRazorCompletionFactsServiceTest(ITestOutputHelper testOutput public void GetDirectiveCompletionItems_AllProvidersCompletionItems() { // Arrange + var sourceDocument = RazorSourceDocument.Create("", RazorSourceDocumentProperties.Default); + var codeDocument = RazorCodeDocument.Create(sourceDocument); var syntaxTree = RazorSyntaxTree.Parse(TestRazorSourceDocument.Create()); var tagHelperDocumentContext = TagHelperDocumentContext.Create(prefix: null, tagHelpers: []); @@ -26,7 +28,7 @@ public void GetDirectiveCompletionItems_AllProvidersCompletionItems() commitCharacters: [], isSnippet: false); - var context = new RazorCompletionContext(AbsoluteIndex: 0, Owner: null, syntaxTree, tagHelperDocumentContext); + var context = new RazorCompletionContext(codeDocument, AbsoluteIndex: 0, Owner: null, SyntaxTree: syntaxTree, TagHelperDocumentContext: tagHelperDocumentContext); var provider1 = StrictMock.Of(p => p.GetCompletionItems(context) == ImmutableArray.Create(completionItem1)); diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeCompletionItemProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeCompletionItemProviderTest.cs index 14a0c9b7301..ff11d166df8 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeCompletionItemProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeCompletionItemProviderTest.cs @@ -363,7 +363,7 @@ private RazorCompletionContext CreateRazorCompletionContext(TestCode testCode) var owner = syntaxTree.Root.FindInnermostNode(testCode.Position, includeWhitespace: true, walkMarkersBack: true); owner = AbstractRazorCompletionFactsService.AdjustSyntaxNodeForWordBoundary(owner, testCode.Position); - return new RazorCompletionContext(testCode.Position, owner, syntaxTree, tagHelperContext); + return new RazorCompletionContext(codeDocument, testCode.Position, owner, syntaxTree, tagHelperContext); } private RazorSyntaxNode GetOwner(string testCodeText) diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeEventParameterCompletionItemProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeEventParameterCompletionItemProviderTest.cs index e6247791e3b..44ea42e429a 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeEventParameterCompletionItemProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeEventParameterCompletionItemProviderTest.cs @@ -211,6 +211,6 @@ private RazorCompletionContext CreateRazorCompletionContext(TestCode documentCon var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex); owner = AbstractRazorCompletionFactsService.AdjustSyntaxNodeForWordBoundary(owner, absoluteIndex); - return new RazorCompletionContext(absoluteIndex, owner, syntaxTree, tagHelperDocumentContext); + return new RazorCompletionContext(codeDocument, absoluteIndex, owner, syntaxTree, tagHelperDocumentContext); } } diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeParameterCompletionItemProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeParameterCompletionItemProviderTest.cs index 8404669f87b..bf8c5ddc413 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeParameterCompletionItemProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeParameterCompletionItemProviderTest.cs @@ -178,6 +178,6 @@ private RazorCompletionContext CreateRazorCompletionContext(int absoluteIndex, s var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex); owner = AbstractRazorCompletionFactsService.AdjustSyntaxNodeForWordBoundary(owner, absoluteIndex); - return new RazorCompletionContext(absoluteIndex, owner, syntaxTree, tagHelperContext); + return new RazorCompletionContext(codeDocument, absoluteIndex, owner, syntaxTree, tagHelperContext); } } diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeTransitionCompletionItemProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeTransitionCompletionItemProviderTest.cs index 5ffecf874f8..2bfd50c89ca 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeTransitionCompletionItemProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeTransitionCompletionItemProviderTest.cs @@ -327,7 +327,7 @@ public void GetCompletionItems_WithAvoidExplicitCommitOption_ReturnsAppropriateC } } - private static RazorSyntaxTree GetSyntaxTree(string text, RazorFileKind? fileKind = null) + private static RazorCodeDocument GetCodeDocument(string text, RazorFileKind? fileKind = null) { var fileKindValue = fileKind ?? RazorFileKind.Component; @@ -340,17 +340,16 @@ private static RazorSyntaxTree GetSyntaxTree(string text, RazorFileKind? fileKin }); }); - var codeDocument = projectEngine.Process(sourceDocument, fileKindValue, importSources: default, tagHelpers: []); - - return codeDocument.GetRequiredSyntaxTree(); + return projectEngine.Process(sourceDocument, fileKindValue, importSources: default, tagHelpers: []); } private RazorCompletionContext CreateContext(int absoluteIndex, string documentContent, RazorFileKind? fileKind = null) { - var syntaxTree = GetSyntaxTree(documentContent, fileKind); + var codeDocument = GetCodeDocument(documentContent, fileKind); + var syntaxTree = codeDocument.GetRequiredSyntaxTree(); var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex, includeWhitespace: true, walkMarkersBack: true); owner = AbstractRazorCompletionFactsService.AdjustSyntaxNodeForWordBoundary(owner, absoluteIndex); - return new RazorCompletionContext(absoluteIndex, owner, syntaxTree, _tagHelperDocumentContext); + return new RazorCompletionContext(codeDocument, absoluteIndex, owner, syntaxTree, _tagHelperDocumentContext); } } diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveCompletionItemProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveCompletionItemProviderTest.cs index 030db94ff85..4ae75d52808 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveCompletionItemProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveCompletionItemProviderTest.cs @@ -430,10 +430,13 @@ public void IsDirectiveCompletableToken_ReturnsFalseForInvalidCSharpTokens() private static RazorCompletionContext CreateRazorCompletionContext(int absoluteIndex, RazorSyntaxTree syntaxTree, CompletionReason reason = CompletionReason.Invoked) { + var sourceDocument = RazorSourceDocument.Create("", RazorSourceDocumentProperties.Default); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var tagHelperDocumentContext = TagHelperDocumentContext.Create(prefix: string.Empty, tagHelpers: []); var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex); owner = AbstractRazorCompletionFactsService.AdjustSyntaxNodeForWordBoundary(owner, absoluteIndex); - return new RazorCompletionContext(absoluteIndex, owner, syntaxTree, tagHelperDocumentContext, reason); + return new RazorCompletionContext(codeDocument, absoluteIndex, owner, syntaxTree, tagHelperDocumentContext, reason); } private static void AssertRazorCompletionItem(string completionDisplayText, DirectiveDescriptor directive, RazorCompletionItem item, ImmutableArray commitCharacters = default, bool isSnippet = false) diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/MarkupTransitionCompletionItemProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/MarkupTransitionCompletionItemProviderTest.cs index 72b824ae8bb..52263f54954 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/MarkupTransitionCompletionItemProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/MarkupTransitionCompletionItemProviderTest.cs @@ -326,11 +326,14 @@ private static void AssertRazorCompletionItem(RazorCompletionItem item) private static RazorCompletionContext CreateRazorCompletionContext(int absoluteIndex, RazorSyntaxTree syntaxTree) { + var sourceDocument = RazorSourceDocument.Create("", RazorSourceDocumentProperties.Default); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var tagHelperDocumentContext = TagHelperDocumentContext.Create(prefix: string.Empty, tagHelpers: []); var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex, includeWhitespace: true, walkMarkersBack: true); owner = AbstractRazorCompletionFactsService.AdjustSyntaxNodeForWordBoundary(owner, absoluteIndex); - return new RazorCompletionContext(absoluteIndex, owner, syntaxTree, tagHelperDocumentContext); + return new RazorCompletionContext(codeDocument, absoluteIndex, owner, syntaxTree, tagHelperDocumentContext); } private static RazorSyntaxTree CreateSyntaxTree(string text, params DirectiveDescriptor[] directives) 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 574f0c1fcab..a010d777616 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 @@ -23,6 +23,8 @@ using Roslyn.Text.Adornments; using Xunit; using Xunit.Abstractions; +using Microsoft.CodeAnalysis.Text; + #if !VSCODE using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.Razor.Snippets; @@ -328,6 +330,67 @@ The end. htmlItemLabels: ["div", "h1"]); } + [Fact] + public async Task Component_FullyQualified() + { + await VerifyCompletionListAsync( + input: """ + This is a Razor document. + + <$$ + + The end. + """, + completionContext: new VSInternalCompletionContext() + { + InvokeKind = VSInternalCompletionInvokeKind.Typing, + TriggerCharacter = "<", + TriggerKind = CompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["EditForm", "SectionOutlet - @using Microsoft.AspNetCore.Components.Sections", "Microsoft.AspNetCore.Components.Sections.SectionOutlet"], + htmlItemLabels: ["div", "h1"], + itemToResolve: "Microsoft.AspNetCore.Components.Sections.SectionOutlet", + expectedResolvedItemDescription: "Microsoft.AspNetCore.Components.Sections.SectionOutlet", + expected: """ + This is a Razor document. + + VerifyCompletionListAsync( Assert.NotNull(item); Assert.NotNull(expectedResolvedItemDescription); - await VerifyCompletionResolveAsync(document, completionListCache, item, expected, expectedResolvedItemDescription); + await VerifyCompletionResolveAsync(document, completionListCache, item, expected, expectedResolvedItemDescription, request.Position); } return result; @@ -919,7 +982,7 @@ private async Task VerifyCompletionListParamsTypeAsync( } } - private async Task VerifyCompletionResolveAsync(CodeAnalysis.TextDocument document, CompletionListCache completionListCache, VSInternalCompletionItem item, string? expected, string expectedResolvedItemDescription) + private async Task VerifyCompletionResolveAsync(CodeAnalysis.TextDocument document, CompletionListCache completionListCache, VSInternalCompletionItem item, string? expected, string expectedResolvedItemDescription, Position position) { // We expect data to be a JsonElement, so for tests we have to _not_ strongly type item.Data = JsonSerializer.SerializeToElement(item.Data, JsonHelpers.JsonSerializerOptions); @@ -942,29 +1005,49 @@ private async Task VerifyCompletionResolveAsync(CodeAnalysis.TextDocument docume Assert.NotNull(result); + SourceText? changedText = null; if (result.TextEdit is { Value: TextEdit edit }) { Assert.NotNull(expected); var text = await document.GetTextAsync(DisposalToken).ConfigureAwait(false); - var changedText = text.WithChanges(text.GetTextChange(edit)); - - AssertEx.EqualOrDiff(expected, changedText.ToString()); + changedText = text.WithChanges(text.GetTextChange(edit)); } else if (result.Command is { Arguments: [_, TextEdit textEdit, ..] }) { Assert.NotNull(expected); var text = await document.GetTextAsync(DisposalToken).ConfigureAwait(false); - var changedText = text.WithChanges(text.GetTextChange(textEdit)); + changedText = text.WithChanges(text.GetTextChange(textEdit)); + } + else if (result.InsertText is { } insertText) + { + // We'll let expected be null here, since its just simple text insertion - AssertEx.EqualOrDiff(expected, changedText.ToString()); + var text = await document.GetTextAsync(DisposalToken).ConfigureAwait(false); + var insertIndex = text.GetRequiredAbsoluteIndex(position); + changedText = text.WithChanges(new TextChange(new TextSpan(insertIndex, 0), insertText)); } else if (expected is not null) { Assert.Fail("Expected a TextEdit or Command with TextEdit, but got none. Presumably resolve failed. Result: " + JsonSerializer.SerializeToElement(result).ToString()); } + if (result.AdditionalTextEdits is not null) + { + // Can't be only additional texts. They're additional! + Assert.NotNull(changedText); + Assert.NotNull(expected); + + changedText = changedText.WithChanges(result.AdditionalTextEdits.Select(changedText.GetTextChange)); + } + + if (expected is not null) + { + Assert.NotNull(changedText); + AssertEx.EqualOrDiff(expected, changedText.ToString()); + } + if (result.Description is not null) { AssertEx.EqualOrDiff(expectedResolvedItemDescription, FlattenDescription(result.Description));