From 3a0d65c1ab64b4608e3d64237dc3f3988f5b595e Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 13 Mar 2025 14:03:57 -0700 Subject: [PATCH 1/5] Make C# completion list rewriting synchronous C# completion list is currently split across three "rewriter" objects that are async. However, none of them need to be async if they're passed a RazorCodeDocument. So, this change acquires a RazorCodeDocument up front before calling the rewriters. With this in place, the rewriters can be synchronous. --- .../Delegation/DelegatedCompletionHelper.cs | 9 +++++---- .../DesignTimeHelperResponseRewriter.cs | 19 +++++++++---------- ...legatedCSharpCompletionResponseRewriter.cs | 11 ++++------- .../Delegation/SnippetResponseRewriter.cs | 13 +++++-------- .../Delegation/TextEditResponseRewriter.cs | 13 +++++-------- .../Extensions/RazorCodeDocumentExtensions.cs | 6 ++++++ 6 files changed, 34 insertions(+), 37 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs index 658d6f6c4bc..1bcef68574f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs @@ -113,17 +113,18 @@ public static async ValueTask RewriteCSharpResponseAsy return new VSInternalCompletionList() { IsIncomplete = true, Items = [] }; } + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + var rewrittenResponse = delegatedResponse; foreach (var rewriter in s_delegatedCSharpCompletionResponseRewriters) { - rewrittenResponse = await rewriter.RewriteAsync( + rewrittenResponse = rewriter.Rewrite( rewrittenResponse, + codeDocument, absoluteIndex, - documentContext, projectedPosition, - completionOptions, - cancellationToken).ConfigureAwait(false); + completionOptions); } return rewrittenResponse; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DesignTimeHelperResponseRewriter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DesignTimeHelperResponseRewriter.cs index 50142f15bde..55580f43d63 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DesignTimeHelperResponseRewriter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DesignTimeHelperResponseRewriter.cs @@ -3,12 +3,10 @@ using System.Collections.Frozen; using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; @@ -32,23 +30,24 @@ internal class DesignTimeHelperResponseRewriter : IDelegatedCSharpCompletionResp "BuildRenderTree" }.ToFrozenSet(); - public async Task RewriteAsync( + public VSInternalCompletionList Rewrite( VSInternalCompletionList completionList, + RazorCodeDocument codeDocument, int hostDocumentIndex, - DocumentContext hostDocumentContext, Position projectedPosition, - RazorCompletionOptions completionOptions, - CancellationToken cancellationToken) + RazorCompletionOptions completionOptions) { - var syntaxTree = await hostDocumentContext.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var owner = syntaxTree.Root.FindInnermostNode(hostDocumentIndex); + var owner = codeDocument + .GetRequiredSyntaxRoot() + .FindInnermostNode(hostDocumentIndex); + if (owner is null) { Debug.Fail("Owner should never be null."); return completionList; } - var sourceText = await hostDocumentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); + var sourceText = codeDocument.Source.Text; // We should remove Razor design-time helpers from C#'s completion list. If the current identifier // being targeted does not start with a double underscore, we trim out all items starting with "__" diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/IDelegatedCSharpCompletionResponseRewriter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/IDelegatedCSharpCompletionResponseRewriter.cs index ad29dcff93d..4779ed134f8 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/IDelegatedCSharpCompletionResponseRewriter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/IDelegatedCSharpCompletionResponseRewriter.cs @@ -1,20 +1,17 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Completion.Delegation; internal interface IDelegatedCSharpCompletionResponseRewriter { - Task RewriteAsync( + VSInternalCompletionList Rewrite( VSInternalCompletionList completionList, + RazorCodeDocument codeDocument, int hostDocumentIndex, - DocumentContext hostDocumentContext, Position projectedPosition, - RazorCompletionOptions completionOptions, - CancellationToken cancellationToken); + RazorCompletionOptions completionOptions); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/SnippetResponseRewriter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/SnippetResponseRewriter.cs index 0b090d4e363..829f5b5e9de 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/SnippetResponseRewriter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/SnippetResponseRewriter.cs @@ -1,10 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System.Threading; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Completion.Delegation; @@ -17,13 +15,12 @@ namespace Microsoft.CodeAnalysis.Razor.Completion.Delegation; /// internal class SnippetResponseRewriter : IDelegatedCSharpCompletionResponseRewriter { - public Task RewriteAsync( + public VSInternalCompletionList Rewrite( VSInternalCompletionList completionList, + RazorCodeDocument codeDocument, int hostDocumentIndex, - DocumentContext hostDocumentContext, Position projectedPosition, - RazorCompletionOptions completionOptions, - CancellationToken cancellationToken) + RazorCompletionOptions completionOptionsn) { using var items = new PooledArrayBuilder(completionList.Items.Length); @@ -43,6 +40,6 @@ public Task RewriteAsync( completionList.Items = items.ToArray(); } - return Task.FromResult(completionList); + return completionList; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/TextEditResponseRewriter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/TextEditResponseRewriter.cs index 120c4793638..62e9cf521cd 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/TextEditResponseRewriter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/TextEditResponseRewriter.cs @@ -1,24 +1,21 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Completion.Delegation; internal class TextEditResponseRewriter : IDelegatedCSharpCompletionResponseRewriter { - public async Task RewriteAsync( + public VSInternalCompletionList Rewrite( VSInternalCompletionList completionList, + RazorCodeDocument codeDocument, int hostDocumentIndex, - DocumentContext hostDocumentContext, Position projectedPosition, - RazorCompletionOptions completionOptions, - CancellationToken cancellationToken) + RazorCompletionOptions completionOptions) { - var sourceText = await hostDocumentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); + var sourceText = codeDocument.Source.Text; var hostDocumentPosition = sourceText.GetPosition(hostDocumentIndex); completionList = TranslateTextEdits(hostDocumentPosition, projectedPosition, completionList); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs index e7a8c0ccea8..26ecac7bf81 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs @@ -22,6 +22,12 @@ internal static class RazorCodeDocumentExtensions { private static readonly object s_csharpSyntaxTreeKey = new(); + public static RazorSyntaxTree GetRequiredSyntaxTree(this RazorCodeDocument codeDocument) + => codeDocument.GetSyntaxTree().AssumeNotNull(); + + public static Syntax.SyntaxNode GetRequiredSyntaxRoot(this RazorCodeDocument codeDocument) + => codeDocument.GetRequiredSyntaxTree().Root; + public static SourceText GetCSharpSourceText(this RazorCodeDocument document) => document.GetCSharpDocument().Text; From ef4a827f8fdba24d95e9b1768666013e228f2fe7 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 13 Mar 2025 14:26:44 -0700 Subject: [PATCH 2/5] Make RewriteCSharpResponseAsync synchronously Now the completion list rewriters are synchronous, the RazorCodeDocument can be acquired by callers of RewriteCSharpResponseAsync and passed to it. Then, RewriteCSharpResponseAsync can be made synchronous. --- .../Delegation/DelegatedCompletionListProvider.cs | 14 ++++---------- .../Delegation/DelegatedCompletionHelper.cs | 9 +++------ .../Completion/RemoteCompletionService.cs | 10 +++++----- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs index 936d423e8c6..ff8796d7e5e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs @@ -81,12 +81,13 @@ public DelegatedCompletionListProvider( completionContext = rewrittenContext; - var razorCodeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + // It's a bit confusing, but we have two different "add snippets" options - one is a part of // RazorCompletionOptions and becomes a part of RazorCompletionContext and is used by // RazorCompletionFactsService, and the second one below that's used for delegated completion // Their values are not related in any way. - var shouldIncludeDelegationSnippets = DelegatedCompletionHelper.ShouldIncludeSnippets(razorCodeDocument, absoluteIndex); + var shouldIncludeDelegationSnippets = DelegatedCompletionHelper.ShouldIncludeSnippets(codeDocument, absoluteIndex); var delegatedParams = new DelegatedCompletionParams( documentContext.GetTextDocumentIdentifierAndVersion(), @@ -103,14 +104,7 @@ public DelegatedCompletionListProvider( cancellationToken).ConfigureAwait(false); var rewrittenResponse = delegatedParams.ProjectedKind == RazorLanguageKind.CSharp - ? await DelegatedCompletionHelper.RewriteCSharpResponseAsync( - delegatedResponse, - absoluteIndex, - documentContext, - delegatedParams.ProjectedPosition, - razorCompletionOptions, - cancellationToken) - .ConfigureAwait(false) + ? DelegatedCompletionHelper.RewriteCSharpResponse(delegatedResponse, absoluteIndex, codeDocument, delegatedParams.ProjectedPosition, razorCompletionOptions) : DelegatedCompletionHelper.RewriteHtmlResponse(delegatedResponse, razorCompletionOptions); var completionCapability = clientCapabilities?.TextDocument?.Completion as VSInternalCompletionSetting; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs index 1bcef68574f..bfad6bd198c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs @@ -96,13 +96,12 @@ internal static class DelegatedCompletionHelper /// /// Possibly modified completion response. /// - public static async ValueTask RewriteCSharpResponseAsync( + public static VSInternalCompletionList RewriteCSharpResponse( VSInternalCompletionList? delegatedResponse, int absoluteIndex, - DocumentContext documentContext, + RazorCodeDocument codeDocument, Position projectedPosition, - RazorCompletionOptions completionOptions, - CancellationToken cancellationToken) + RazorCompletionOptions completionOptions) { if (delegatedResponse?.Items is null) { @@ -113,8 +112,6 @@ public static async ValueTask RewriteCSharpResponseAsy return new VSInternalCompletionList() { IsIncomplete = true, Items = [] }; } - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - var rewrittenResponse = delegatedResponse; foreach (var rewriter in s_delegatedCSharpCompletionResponseRewriters) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs index 62f57bac2d6..c5c9a77c74a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs @@ -212,16 +212,16 @@ private async ValueTask GetCompletionAsync( }; } + var codeDocument = await remoteDocumentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + var vsPlatformCompletionList = JsonHelpers.ToVsLSP(roslynCompletionList); - var rewrittenResponse = await DelegatedCompletionHelper.RewriteCSharpResponseAsync( + var rewrittenResponse = DelegatedCompletionHelper.RewriteCSharpResponse( vsPlatformCompletionList, documentIndex, - remoteDocumentContext, + codeDocument, mappedPosition, - razorCompletionOptions, - cancellationToken) - .ConfigureAwait(false); + razorCompletionOptions); return rewrittenResponse; } From b5333632d5b133c35f1bd033d6118e4a1f5addd8 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 13 Mar 2025 15:18:41 -0700 Subject: [PATCH 3/5] Make TryGetProvisionalCompletionInfoAsync synchronous The only reason this method was async was to call the IDocumentMappingService extension method, GetPositionInfoAsync. However, the reason *that* extension method is async is to call GetCodeDocumentAsync(). Ultimately, this method can be made synchronous by passing RazorCodeDocument to it. --- .../DelegatedCompletionListProvider.cs | 16 ++++--------- .../Delegation/DelegatedCompletionHelper.cs | 24 +++++++++---------- .../Completion/RemoteCompletionService.cs | 14 +++-------- 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs index ff8796d7e5e..2814b354db3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs @@ -61,17 +61,13 @@ public DelegatedCompletionListProvider( return null; } - var provisionalCompletion = await DelegatedCompletionHelper.TryGetProvisionalCompletionInfoAsync( - documentContext, - completionContext, - positionInfo, - _documentMappingService, - cancellationToken).ConfigureAwait(false); + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + TextEdit? provisionalTextEdit = null; - if (provisionalCompletion is { } provisionalCompletionValue) + if (DelegatedCompletionHelper.TryGetProvisionalCompletionInfo(codeDocument, completionContext, positionInfo, _documentMappingService, out var provisionalCompletion)) { - provisionalTextEdit = provisionalCompletionValue.ProvisionalTextEdit; - positionInfo = provisionalCompletionValue.DocumentPositionInfo; + provisionalTextEdit = provisionalCompletion.ProvisionalTextEdit; + positionInfo = provisionalCompletion.DocumentPositionInfo; } if (DelegatedCompletionHelper.RewriteContext(completionContext, positionInfo.LanguageKind, _triggerAndCommitCharacters) is not { } rewrittenContext) @@ -81,8 +77,6 @@ public DelegatedCompletionListProvider( completionContext = rewrittenContext; - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - // It's a bit confusing, but we have two different "add snippets" options - one is a part of // RazorCompletionOptions and becomes a part of RazorCompletionContext and is used by // RazorCompletionFactsService, and the second one below that's used for delegated completion diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs index bfad6bd198c..6b460666425 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs @@ -3,11 +3,8 @@ using System.Collections.Immutable; using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Protocol.Completion; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -159,34 +156,34 @@ public static VSInternalCompletionList RewriteHtmlResponse( /// to C# and will return a temporary edit that should be made to the generated document /// in order to add the '.' to the generated C# contents. /// - public static async Task TryGetProvisionalCompletionInfoAsync( - DocumentContext documentContext, + public static bool TryGetProvisionalCompletionInfo( + RazorCodeDocument codeDocument, VSInternalCompletionContext completionContext, DocumentPositionInfo originalPositionInfo, IDocumentMappingService documentMappingService, - CancellationToken cancellationToken) + out CompletionPositionInfo result) { + result = default; + if (originalPositionInfo.LanguageKind != RazorLanguageKind.Html || completionContext.TriggerKind != CompletionTriggerKind.TriggerCharacter || completionContext.TriggerCharacter != ".") { // Invalid provisional completion context - return null; + return false; } if (originalPositionInfo.Position.Character == 0) { // We're at the start of line. Can't have provisional completions here. - return null; + return false; } - var previousCharacterPositionInfo = await documentMappingService - .GetPositionInfoAsync(documentContext, originalPositionInfo.HostDocumentIndex - 1, cancellationToken) - .ConfigureAwait(false); + var previousCharacterPositionInfo = documentMappingService.GetPositionInfo(codeDocument, originalPositionInfo.HostDocumentIndex - 1); if (previousCharacterPositionInfo.LanguageKind != RazorLanguageKind.CSharp) { - return null; + return false; } var previousPosition = previousCharacterPositionInfo.Position; @@ -202,7 +199,8 @@ public static VSInternalCompletionList RewriteHtmlResponse( previousPosition.Character + 1), previousCharacterPositionInfo.HostDocumentIndex + 1); - return new CompletionPositionInfo(addProvisionalDot, provisionalPositionInfo, ShouldIncludeDelegationSnippets: false); + result = new CompletionPositionInfo(addProvisionalDot, provisionalPositionInfo, ShouldIncludeDelegationSnippets: false); + return true; } public static bool ShouldIncludeSnippets(RazorCodeDocument razorCodeDocument, int absoluteIndex) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs index c5c9a77c74a..cb85c92fe88 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs @@ -63,18 +63,10 @@ protected override IRemoteCompletionService CreateService(in ServiceArgs args) var positionInfo = GetPositionInfo(codeDocument, index); if (positionInfo.LanguageKind != RazorLanguageKind.Razor - && await DelegatedCompletionHelper.TryGetProvisionalCompletionInfoAsync( - remoteDocumentContext, - completionContext, - positionInfo, - DocumentMappingService, - cancellationToken) - .ConfigureAwait(false) is { } provisionalCompletionInfo) + && DelegatedCompletionHelper.TryGetProvisionalCompletionInfo( + codeDocument, completionContext, positionInfo, DocumentMappingService, out var provisionalCompletionInfo)) { - return new CompletionPositionInfo( - provisionalCompletionInfo.ProvisionalTextEdit, - provisionalCompletionInfo.DocumentPositionInfo, - ShouldIncludeDelegationSnippets: false); + return provisionalCompletionInfo with { ShouldIncludeDelegationSnippets = false }; } var shouldIncludeSnippets = positionInfo.LanguageKind == RazorLanguageKind.Html From 4a33dc1b11f78e435811994a0dd22370dd567731 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 13 Mar 2025 15:23:37 -0700 Subject: [PATCH 4/5] Remove IDocumentMappingService.GetPositionInfoAsync extension method This method is async just to call DocumentContext.GetCodeDocumentAsync and the only caller has a RazorCodeDocument in hand. So, this method can be removed in favor of the synchronous version. --- .../Delegation/DelegatedCompletionListProvider.cs | 7 ++----- .../IDocumentMappingServiceExtensions.cs | 10 ---------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs index 2814b354db3..82808e816c5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs @@ -51,18 +51,15 @@ public DelegatedCompletionListProvider( Guid correlationId, CancellationToken cancellationToken) { - var positionInfo = await _documentMappingService - .GetPositionInfoAsync(documentContext, absoluteIndex, cancellationToken) - .ConfigureAwait(false); + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + var positionInfo = _documentMappingService.GetPositionInfo(codeDocument, absoluteIndex); if (positionInfo.LanguageKind == RazorLanguageKind.Razor) { // Nothing to delegate to. return null; } - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - TextEdit? provisionalTextEdit = null; if (DelegatedCompletionHelper.TryGetProvisionalCompletionInfo(codeDocument, completionContext, positionInfo, _documentMappingService, out var provisionalCompletion)) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/IDocumentMappingServiceExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/IDocumentMappingServiceExtensions.cs index 7293971c6bd..e83f826b2ec 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/IDocumentMappingServiceExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/IDocumentMappingServiceExtensions.cs @@ -5,11 +5,8 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -35,13 +32,6 @@ public static bool TryMapToHostDocumentRange(this IDocumentMappingService servic public static bool TryMapToHostDocumentRange(this IDocumentMappingService service, IRazorGeneratedDocument generatedDocument, Range projectedRange, [NotNullWhen(true)] out Range? originalRange) => service.TryMapToHostDocumentRange(generatedDocument, projectedRange, MappingBehavior.Strict, out originalRange); - public static async Task GetPositionInfoAsync(this IDocumentMappingService service, DocumentContext documentContext, int hostDocumentIndex, CancellationToken cancellationToken) - { - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - - return service.GetPositionInfo(codeDocument, hostDocumentIndex); - } - public static DocumentPositionInfo GetPositionInfo( this IDocumentMappingService service, RazorCodeDocument codeDocument, From 09542f89640ddfbf880116aa93a1b544877a5252 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 13 Mar 2025 16:40:12 -0700 Subject: [PATCH 5/5] Audit GetCompletionListAsync methods This change audits and refactors to the GetCompletionListAsync methods on CompletionListProvider, RazorCompletionListProvider and DelegatedCompletionListProvider in favor of removing async state machines where possible. --- .../RazorCompletionBenchmark.cs | 18 ++--- .../Completion/CompletionListProvider.cs | 80 ++++++++++++++----- .../DelegatedCompletionListProvider.cs | 52 +++++++++--- .../Completion/RazorCompletionEndpoint.cs | 59 +++++++------- .../Completion/RazorCompletionListProvider.cs | 20 ++--- .../Extensions/RazorCodeDocumentExtensions.cs | 3 + .../Completion/RemoteCompletionService.cs | 76 ++++++++++++------ .../Completion/CompletionListProviderTest.cs | 17 ++-- ...legatedCompletionItemResolverTest.NetFx.cs | 3 +- .../DelegatedCompletionListProviderTest.cs | 9 +++ .../Delegation/ResponseRewriterTestBase.cs | 7 +- .../Completion/RazorCompletionEndpointTest.cs | 5 +- .../RazorCompletionListProviderTest.cs | 78 ++++++++---------- 13 files changed, 262 insertions(+), 165 deletions(-) diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCompletionBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCompletionBenchmark.cs index df80ea251a4..6f9bbe4f465 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCompletionBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCompletionBenchmark.cs @@ -7,12 +7,14 @@ using System.Threading; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer; using Microsoft.AspNetCore.Razor.LanguageServer.Completion; using Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; @@ -48,7 +50,7 @@ public async Task SetupAsync() var completionListProvider = new CompletionListProvider(razorCompletionListProvider, delegatedCompletionListProvider, triggerAndCommitCharacters); var configurationService = new DefaultRazorConfigurationService(clientConnection, loggerFactory); var optionsMonitor = new RazorLSPOptionsMonitor(configurationService, RazorLSPOptions.Default); - CompletionEndpoint = new RazorCompletionEndpoint(completionListProvider, triggerAndCommitCharacters, telemetryReporter: null, optionsMonitor); + CompletionEndpoint = new RazorCompletionEndpoint(completionListProvider, triggerAndCommitCharacters, NoOpTelemetryReporter.Instance, optionsMonitor); var clientCapabilities = new VSInternalClientCapabilities { @@ -145,12 +147,13 @@ public TestDelegatedCompletionListProvider( IDocumentMappingService documentMappingService, IClientConnection clientConnection, CompletionListCache completionListCache, - CompletionTriggerAndCommitCharacters completionTriggerAndCommitCharacters) - : base(documentMappingService, clientConnection, completionListCache, completionTriggerAndCommitCharacters) + CompletionTriggerAndCommitCharacters triggerAndCommitCharacters) + : base(documentMappingService, clientConnection, completionListCache, triggerAndCommitCharacters) { } - public override Task GetCompletionListAsync( + public override ValueTask GetCompletionListAsync( + RazorCodeDocument codeDocument, int absoluteIndex, VSInternalCompletionContext completionContext, DocumentContext documentContext, @@ -158,11 +161,6 @@ public TestDelegatedCompletionListProvider( RazorCompletionOptions completionOptions, Guid correlationId, CancellationToken cancellationToken) - { - return Task.FromResult( - new VSInternalCompletionList - { - }); - } + => new(new VSInternalCompletionList()); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListProvider.cs index 224535bd45f..fe11338aef7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -22,7 +23,7 @@ internal class CompletionListProvider( private readonly DelegatedCompletionListProvider _delegatedCompletionListProvider = delegatedCompletionListProvider; private readonly CompletionTriggerAndCommitCharacters _triggerAndCommitCharacters = triggerAndCommitCharacters; - public async Task GetCompletionListAsync( + public ValueTask GetCompletionListAsync( int absoluteIndex, VSInternalCompletionContext completionContext, DocumentContext documentContext, @@ -31,37 +32,78 @@ internal class CompletionListProvider( Guid correlationId, CancellationToken cancellationToken) { - // First we delegate to get completion items from the individual language server - var delegatedCompletionList = _triggerAndCommitCharacters.IsValidDelegationTrigger(completionContext) - ? await _delegatedCompletionListProvider.GetCompletionListAsync( + var isDelegationTrigger = _triggerAndCommitCharacters.IsValidDelegationTrigger(completionContext); + var isRazorTrigger = _triggerAndCommitCharacters.IsValidRazorTrigger(completionContext); + + // We don't have a valid trigger, so we can't provide completions + return isDelegationTrigger || isRazorTrigger + ? new(GetCompletionListCoreAsync( absoluteIndex, completionContext, documentContext, clientCapabilities, razorCompletionOptions, correlationId, - cancellationToken).ConfigureAwait(false) - : null; + isDelegationTrigger, + isRazorTrigger, + cancellationToken)) + : default; + } + + private async Task GetCompletionListCoreAsync( + int absoluteIndex, + VSInternalCompletionContext completionContext, + DocumentContext documentContext, + VSInternalClientCapabilities clientCapabilities, + RazorCompletionOptions razorCompletionOptions, + Guid correlationId, + bool isDelegationTrigger, + bool isRazorTrigger, + CancellationToken cancellationToken) + { + Debug.Assert(isDelegationTrigger || isRazorTrigger); + + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + // First we delegate to get completion items from the individual language server + VSInternalCompletionList? delegatedCompletionList = null; + HashSet? existingItems = null; - // Extract the items we got back from the delegated server, to inform tag helper completion - var existingItems = delegatedCompletionList?.Items != null - ? new HashSet(delegatedCompletionList.Items.Select(i => i.Label)) - : null; + if (isDelegationTrigger) + { + delegatedCompletionList = await _delegatedCompletionListProvider + .GetCompletionListAsync( + codeDocument, + absoluteIndex, + completionContext, + documentContext, + clientCapabilities, + razorCompletionOptions, + correlationId, + cancellationToken) + .ConfigureAwait(false); + + // Extract the items we got back from the delegated server, to inform tag helper completion + if (delegatedCompletionList?.Items is { } delegatedItems) + { + existingItems = [.. delegatedItems.Select(static i => i.Label)]; + } + } // Now we get the Razor completion list, using information from the actual language server if necessary - var razorCompletionList = _triggerAndCommitCharacters.IsValidRazorTrigger(completionContext) - ? await _razorCompletionListProvider.GetCompletionListAsync( + VSInternalCompletionList? razorCompletionList = null; + + if (isRazorTrigger) + { + razorCompletionList = _razorCompletionListProvider.GetCompletionList( + codeDocument, absoluteIndex, completionContext, - documentContext, clientCapabilities, existingItems, - razorCompletionOptions, - cancellationToken).ConfigureAwait(false) - : null; - - var finalCompletionList = CompletionListMerger.Merge(razorCompletionList, delegatedCompletionList); + razorCompletionOptions); + } - return finalCompletionList; + return CompletionListMerger.Merge(razorCompletionList, delegatedCompletionList); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs index 82808e816c5..a70b1ea63ef 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs @@ -42,7 +42,8 @@ public DelegatedCompletionListProvider( } // virtual for tests - public virtual async Task GetCompletionListAsync( + public virtual ValueTask GetCompletionListAsync( + RazorCodeDocument codeDocument, int absoluteIndex, VSInternalCompletionContext completionContext, DocumentContext documentContext, @@ -51,13 +52,11 @@ public DelegatedCompletionListProvider( Guid correlationId, CancellationToken cancellationToken) { - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - var positionInfo = _documentMappingService.GetPositionInfo(codeDocument, absoluteIndex); if (positionInfo.LanguageKind == RazorLanguageKind.Razor) { // Nothing to delegate to. - return null; + return default; } TextEdit? provisionalTextEdit = null; @@ -69,7 +68,7 @@ public DelegatedCompletionListProvider( if (DelegatedCompletionHelper.RewriteContext(completionContext, positionInfo.LanguageKind, _triggerAndCommitCharacters) is not { } rewrittenContext) { - return null; + return default; } completionContext = rewrittenContext; @@ -80,8 +79,35 @@ public DelegatedCompletionListProvider( // Their values are not related in any way. var shouldIncludeDelegationSnippets = DelegatedCompletionHelper.ShouldIncludeSnippets(codeDocument, absoluteIndex); - var delegatedParams = new DelegatedCompletionParams( + return new(GetDelegatedCompletionListAsync( + codeDocument, + absoluteIndex, + completionContext, documentContext.GetTextDocumentIdentifierAndVersion(), + clientCapabilities, + razorCompletionOptions, + correlationId, + positionInfo, + provisionalTextEdit, + shouldIncludeDelegationSnippets, + cancellationToken)); + } + + private async Task GetDelegatedCompletionListAsync( + RazorCodeDocument codeDocument, + int absoluteIndex, + VSInternalCompletionContext completionContext, + TextDocumentIdentifierAndVersion identifier, + VSInternalClientCapabilities clientCapabilities, + RazorCompletionOptions razorCompletionOptions, + Guid correlationId, + DocumentPositionInfo positionInfo, + TextEdit? provisionalTextEdit, + bool shouldIncludeDelegationSnippets, + CancellationToken cancellationToken) + { + var delegatedParams = new DelegatedCompletionParams( + identifier, positionInfo.Position, positionInfo.LanguageKind, completionContext, @@ -89,13 +115,15 @@ public DelegatedCompletionListProvider( shouldIncludeDelegationSnippets, correlationId); - var delegatedResponse = await _clientConnection.SendRequestAsync( - LanguageServerConstants.RazorCompletionEndpointName, - delegatedParams, - cancellationToken).ConfigureAwait(false); + var delegatedResponse = await _clientConnection + .SendRequestAsync( + LanguageServerConstants.RazorCompletionEndpointName, + delegatedParams, + cancellationToken) + .ConfigureAwait(false); - var rewrittenResponse = delegatedParams.ProjectedKind == RazorLanguageKind.CSharp - ? DelegatedCompletionHelper.RewriteCSharpResponse(delegatedResponse, absoluteIndex, codeDocument, delegatedParams.ProjectedPosition, razorCompletionOptions) + var rewrittenResponse = positionInfo.LanguageKind == RazorLanguageKind.CSharp + ? DelegatedCompletionHelper.RewriteCSharpResponse(delegatedResponse, absoluteIndex, codeDocument, positionInfo.Position, razorCompletionOptions) : DelegatedCompletionHelper.RewriteHtmlResponse(delegatedResponse, razorCompletionOptions); var completionCapability = clientCapabilities?.TextDocument?.Completion as VSInternalCompletionSetting; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionEndpoint.cs index b70d17103f8..5d17d7d3f68 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionEndpoint.cs @@ -19,14 +19,14 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; [RazorLanguageServerEndpoint(Methods.TextDocumentCompletionName)] internal class RazorCompletionEndpoint( CompletionListProvider completionListProvider, - CompletionTriggerAndCommitCharacters completionTriggerAndCommitCharacters, - ITelemetryReporter? telemetryReporter, + CompletionTriggerAndCommitCharacters triggerAndCommitCharacters, + ITelemetryReporter telemetryReporter, RazorLSPOptionsMonitor optionsMonitor) : IRazorRequestHandler, ICapabilitiesProvider { private readonly CompletionListProvider _completionListProvider = completionListProvider; - private readonly CompletionTriggerAndCommitCharacters _triggerAndCommitCharacters = completionTriggerAndCommitCharacters; - private readonly ITelemetryReporter? _telemetryReporter = telemetryReporter; + private readonly CompletionTriggerAndCommitCharacters _triggerAndCommitCharacters = triggerAndCommitCharacters; + private readonly ITelemetryReporter _telemetryReporter = telemetryReporter; private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; private VSInternalClientCapabilities? _clientCapabilities; @@ -52,46 +52,43 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(CompletionParams request public async Task HandleRequestAsync(CompletionParams request, RazorRequestContext requestContext, CancellationToken cancellationToken) { - var documentContext = requestContext.DocumentContext; - - if (request.Context is null || documentContext is null) - { - return null; - } - - var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); - if (!sourceText.TryGetAbsoluteIndex(request.Position, out var hostDocumentIndex)) + if (request.Context is not VSInternalCompletionContext completionContext || + requestContext.DocumentContext is not { } documentContext) { return null; } - if (request.Context is not VSInternalCompletionContext completionContext) + var autoShownCompletion = completionContext.InvokeKind != VSInternalCompletionInvokeKind.Explicit; + var options = _optionsMonitor.CurrentValue; + if (autoShownCompletion && !options.AutoShowCompletion) { - Debug.Fail("Completion context should never be null in practice"); return null; } - var autoShownCompletion = completionContext.InvokeKind != VSInternalCompletionInvokeKind.Explicit; - if (autoShownCompletion && !_optionsMonitor.CurrentValue.AutoShowCompletion) + var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); + if (!sourceText.TryGetAbsoluteIndex(request.Position, out var hostDocumentIndex)) { return null; } var correlationId = Guid.NewGuid(); - using var _ = _telemetryReporter?.TrackLspRequest(Methods.TextDocumentCompletionName, LanguageServerConstants.RazorLanguageServerName, TelemetryThresholds.CompletionRazorTelemetryThreshold, correlationId); + using (_telemetryReporter.TrackLspRequest(Methods.TextDocumentCompletionName, LanguageServerConstants.RazorLanguageServerName, TelemetryThresholds.CompletionRazorTelemetryThreshold, correlationId)) + { + var razorCompletionOptions = new RazorCompletionOptions( + SnippetsSupported: true, + AutoInsertAttributeQuotes: options.AutoInsertAttributeQuotes, + CommitElementsWithSpace: options.CommitElementsWithSpace); - var razorCompletionOptions = new RazorCompletionOptions( - SnippetsSupported: true, - AutoInsertAttributeQuotes: _optionsMonitor.CurrentValue.AutoInsertAttributeQuotes, - CommitElementsWithSpace: _optionsMonitor.CurrentValue.CommitElementsWithSpace); - var completionList = await _completionListProvider.GetCompletionListAsync( - hostDocumentIndex, - completionContext, - documentContext, - _clientCapabilities!, - razorCompletionOptions, - correlationId, - cancellationToken).ConfigureAwait(false); - return completionList; + return await _completionListProvider + .GetCompletionListAsync( + hostDocumentIndex, + completionContext, + documentContext, + _clientCapabilities.AssumeNotNull(), + razorCompletionOptions, + correlationId, + cancellationToken) + .ConfigureAwait(false); + } } } 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 7ec0ccd1a58..821c06282e8 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionListProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionListProvider.cs @@ -7,12 +7,11 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Completion; @@ -32,14 +31,13 @@ internal class RazorCompletionListProvider( }; // virtual for tests - public virtual async Task GetCompletionListAsync( + public virtual VSInternalCompletionList? GetCompletionList( + RazorCodeDocument codeDocument, int absoluteIndex, VSInternalCompletionContext completionContext, - DocumentContext documentContext, VSInternalClientCapabilities clientCapabilities, HashSet? existingCompletions, - RazorCompletionOptions completionOptions, - CancellationToken cancellationToken) + RazorCompletionOptions completionOptions) { if (!IsApplicableTriggerContext(completionContext)) { @@ -54,8 +52,9 @@ internal class RazorCompletionListProvider( _ => CompletionReason.Typing, }; - var syntaxTree = await documentContext.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var tagHelperContext = await documentContext.GetTagHelperContextAsync(cancellationToken).ConfigureAwait(false); + var syntaxTree = codeDocument.GetRequiredSyntaxTree(); + var tagHelperContext = codeDocument.GetRequiredTagHelperContext(); + var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex, includeWhitespace: true, walkMarkersBack: true); owner = AbstractRazorCompletionFactsService.AdjustSyntaxNodeForWordBoundary(owner, absoluteIndex); @@ -77,7 +76,8 @@ internal class RazorCompletionListProvider( var completionCapability = clientCapabilities?.TextDocument?.Completion as VSInternalCompletionSetting; // The completion list is cached and can be retrieved via this result id to enable the resolve completion functionality. - var razorResolveContext = new RazorCompletionResolveContext(documentContext.FilePath, razorCompletionItems); + var filePath = codeDocument.Source.FilePath.AssumeNotNull(); + var razorResolveContext = new RazorCompletionResolveContext(filePath, razorCompletionItems); var resultId = _completionListCache.Add(completionList, razorResolveContext); completionList.SetResultId(resultId, completionCapability); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs index 26ecac7bf81..ddf152ca867 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs @@ -28,6 +28,9 @@ public static RazorSyntaxTree GetRequiredSyntaxTree(this RazorCodeDocument codeD public static Syntax.SyntaxNode GetRequiredSyntaxRoot(this RazorCodeDocument codeDocument) => codeDocument.GetRequiredSyntaxTree().Root; + public static TagHelperDocumentContext GetRequiredTagHelperContext(this RazorCodeDocument codeDocument) + => codeDocument.GetTagHelperContext().AssumeNotNull(); + public static SourceText GetCSharpSourceText(this RazorCodeDocument document) => document.GetCSharpDocument().Text; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs index cb85c92fe88..8ac661d9c3a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.Completion.Delegation; @@ -103,17 +104,36 @@ private async ValueTask GetCompletionAsync( HashSet existingDelegatedCompletions, CancellationToken cancellationToken) { - VSInternalCompletionList? csharpCompletionList = null; var documentPositionInfo = positionInfo.DocumentPositionInfo; - if (documentPositionInfo.LanguageKind == RazorLanguageKind.CSharp && - _triggerAndCommitCharacters.IsValidCSharpTrigger(completionContext)) + + var isCSharpTrigger = documentPositionInfo.LanguageKind == RazorLanguageKind.CSharp && + _triggerAndCommitCharacters.IsValidCSharpTrigger(completionContext); + + var isRazorTrigger = _triggerAndCommitCharacters.IsValidRazorTrigger(completionContext); + + if (!isCSharpTrigger && !isRazorTrigger) + { + // We don't have a valid trigger, so we can't provide completions. + return Response.CallHtml; + } + + var documentSnapshot = remoteDocumentContext.Snapshot; + + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); + + VSInternalCompletionList? csharpCompletionList = null; + if (isCSharpTrigger) { var mappedPosition = documentPositionInfo.Position; + + var csharpGeneratedDocument = await GetCSharpGeneratedDocumentAsync( + documentSnapshot, positionInfo.ProvisionalTextEdit, cancellationToken).ConfigureAwait(false); + csharpCompletionList = await GetCSharpCompletionAsync( - remoteDocumentContext, + csharpGeneratedDocument, + codeDocument, documentPositionInfo.HostDocumentIndex, mappedPosition, - positionInfo.ProvisionalTextEdit, completionContext, razorCompletionOptions, cancellationToken) @@ -126,17 +146,18 @@ private async ValueTask GetCompletionAsync( } } - var razorCompletionList = _triggerAndCommitCharacters.IsValidRazorTrigger(completionContext) - ? await _razorCompletionListProvider.GetCompletionListAsync( + VSInternalCompletionList? razorCompletionList = null; + + if (isRazorTrigger) + { + razorCompletionList = _razorCompletionListProvider.GetCompletionList( + codeDocument, documentPositionInfo.HostDocumentIndex, completionContext, - remoteDocumentContext, _clientCapabilitiesService.ClientCapabilities, existingCompletions: existingDelegatedCompletions, - razorCompletionOptions, - cancellationToken) - .ConfigureAwait(false) - : null; + razorCompletionOptions); + } // Merge won't return anything only if both completion lists passed in are null, // in which case client should just proceed with HTML completion. @@ -149,24 +170,14 @@ private async ValueTask GetCompletionAsync( } private async ValueTask GetCSharpCompletionAsync( - RemoteDocumentContext remoteDocumentContext, + SourceGeneratedDocument generatedDocument, + RazorCodeDocument codeDocument, int documentIndex, Position mappedPosition, - TextEdit? provisionalTextEdit, CompletionContext completionContext, RazorCompletionOptions razorCompletionOptions, CancellationToken cancellationToken) { - var generatedDocument = await remoteDocumentContext.Snapshot - .GetGeneratedDocumentAsync(cancellationToken).ConfigureAwait(false); - if (provisionalTextEdit is not null) - { - var generatedText = await generatedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); - var change = generatedText.GetTextChange(provisionalTextEdit); - generatedText = generatedText.WithChanges([change]); - generatedDocument = (SourceGeneratedDocument)generatedDocument.WithText(generatedText); - } - // This is, to say the least, not ideal. In future we're going to normalize on to Roslyn LSP types, and this can go. if (JsonHelpers.ToRoslynLSP(completionContext) is not { } roslynCompletionContext) { @@ -204,8 +215,6 @@ private async ValueTask GetCompletionAsync( }; } - var codeDocument = await remoteDocumentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - var vsPlatformCompletionList = JsonHelpers.ToVsLSP(roslynCompletionList); var rewrittenResponse = DelegatedCompletionHelper.RewriteCSharpResponse( @@ -217,4 +226,19 @@ private async ValueTask GetCompletionAsync( return rewrittenResponse; } + + private static async Task GetCSharpGeneratedDocumentAsync(RemoteDocumentSnapshot documentSnapshot, TextEdit? provisionalTextEdit, CancellationToken cancellationToken) + { + var generatedDocument = await documentSnapshot.GetGeneratedDocumentAsync(cancellationToken).ConfigureAwait(false); + + if (provisionalTextEdit is not null) + { + var generatedText = await generatedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); + var change = generatedText.GetTextChange(provisionalTextEdit); + generatedText = generatedText.WithChanges([change]); + generatedDocument = (SourceGeneratedDocument)generatedDocument.WithText(generatedText); + } + + return generatedDocument; + } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListProviderTest.cs index 4e12c4f46f5..36243e9d75e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListProviderTest.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; @@ -88,7 +89,8 @@ public TestDelegatedCompletionListProvider(VSInternalCompletionList completionLi _completionList = completionList; } - public override Task GetCompletionListAsync( + public override ValueTask GetCompletionListAsync( + RazorCodeDocument codeDocument, int absoluteIndex, VSInternalCompletionContext completionContext, DocumentContext documentContext, @@ -97,7 +99,7 @@ public override Task GetCompletionListAsync( Guid correlationId, CancellationToken cancellationToken) { - return Task.FromResult(_completionList); + return new(_completionList); } } @@ -113,16 +115,13 @@ public TestRazorCompletionListProvider( _completionList = completionList; } - public override Task GetCompletionListAsync( + public override VSInternalCompletionList GetCompletionList( + RazorCodeDocument codeDocument, int absoluteIndex, VSInternalCompletionContext completionContext, - DocumentContext documentContext, VSInternalClientCapabilities clientCapabilities, HashSet existingCompletions, - RazorCompletionOptions razorCompletionOptions, - CancellationToken cancellationToken) - { - return Task.FromResult(_completionList); - } + RazorCompletionOptions razorCompletionOptions) + => _completionList; } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs index 4d193294843..691d7132c37 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs @@ -234,7 +234,7 @@ public async Task ResolveAsync_Html_Resolves() private async Task ResolveCompletionItemAsync(string content, string itemToResolve, CancellationToken cancellationToken) { TestFileMarkupParser.GetPosition(content, out var documentContent, out var cursorPosition); - var codeDocument = CreateCodeDocument(documentContent); + var codeDocument = CreateCodeDocument(documentContent, filePath: "C:/path/to/file.razor"); await using var csharpServer = await CreateCSharpServerAsync(codeDocument); var server = TestDelegatedCompletionItemResolverServer.Create(csharpServer, DisposalToken); @@ -289,6 +289,7 @@ private async Task CreateCSharpServerAsync(RazorCodeDocumen var provider = TestDelegatedCompletionListProvider.Create(csharpServer, LoggerFactory, DisposalToken); var completionList = await provider.GetCompletionListAsync( + codeDocument, cursorPosition, completionContext, documentContext, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionListProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionListProviderTest.cs index 7960c87e176..e39c30d8148 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionListProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionListProviderTest.cs @@ -50,6 +50,7 @@ public async Task HtmlDelegation_Invoked() // Act await _provider.GetCompletionListAsync( + codeDocument, absoluteIndex: 1, completionContext, documentContext, @@ -83,6 +84,7 @@ public async Task HtmlDelegation_TriggerCharacter() // Act await _provider.GetCompletionListAsync( + codeDocument, absoluteIndex: 1, completionContext, documentContext, @@ -117,6 +119,7 @@ public async Task HtmlDelegation_UnsupportedTriggerCharacter_ReturnsNull() // Act await _provider.GetCompletionListAsync( + codeDocument, absoluteIndex: 1, completionContext, documentContext, @@ -146,6 +149,7 @@ public async Task Delegation_NullResult_ToIncompleteResult() // Act var delegatedCompletionList = await provider.GetCompletionListAsync( + codeDocument, absoluteIndex: 1, completionContext, documentContext, @@ -199,6 +203,7 @@ public async Task RazorDelegation_Noop() // Act var completionList = await _provider.GetCompletionListAsync( + codeDocument, absoluteIndex: 11, completionContext, documentContext, @@ -228,6 +233,7 @@ public async Task ProvisionalCompletion_TranslatesToCSharpWithProvisionalTextEdi // Act await _provider.GetCompletionListAsync( + codeDocument, absoluteIndex: 10, completionContext, documentContext, @@ -264,6 +270,7 @@ public async Task DotTriggerInMiddleOfCSharpImplicitExpressionNotTreatedAsProvis // Act await _provider.GetCompletionListAsync( + codeDocument, absoluteIndex: 10, completionContext, documentContext, @@ -337,6 +344,7 @@ public async Task ShouldIncludeSnippets(string input, bool shouldIncludeSnippets }; await completionProvider.GetCompletionListAsync( + codeDocument, cursorPosition, completionContext, documentContext, @@ -380,6 +388,7 @@ private async Task GetCompletionListAsync(string conte var provider = TestDelegatedCompletionListProvider.Create(csharpServer, LoggerFactory, DisposalToken); var completionList = await provider.GetCompletionListAsync( + codeDocument, absoluteIndex: cursorPosition, completionContext, documentContext, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/ResponseRewriterTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/ResponseRewriterTestBase.cs index 68a0c64e40d..82acc646466 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/ResponseRewriterTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/ResponseRewriterTestBase.cs @@ -39,12 +39,15 @@ private protected async Task GetRewrittenCompletionLis VSInternalCompletionList initialCompletionList, RazorCompletionOptions razorCompletionOptions) { + const string FilePath = "C:/path/to/file.cshtml"; + var completionContext = new VSInternalCompletionContext(); - var codeDocument = CreateCodeDocument(documentContent); - var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml", codeDocument); + var codeDocument = CreateCodeDocument(documentContent, filePath: FilePath); + var documentContext = TestDocumentContext.Create(FilePath, codeDocument); var provider = TestDelegatedCompletionListProvider.Create(initialCompletionList, LoggerFactory); var clientCapabilities = new VSInternalClientCapabilities(); var completionList = await provider.GetCompletionListAsync( + codeDocument, absoluteIndex, completionContext, documentContext, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionEndpointTest.cs index c2d25c7d383..2dbc4d33bd0 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionEndpointTest.cs @@ -6,6 +6,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; @@ -21,7 +22,7 @@ public async Task Handle_NoDocumentContext_NoCompletionItems() // Arrange var documentPath = "C:/path/to/document.cshtml"; var optionsMonitor = GetOptionsMonitor(); - var completionEndpoint = new RazorCompletionEndpoint(completionListProvider: null, completionTriggerAndCommitCharacters: null, telemetryReporter: null, optionsMonitor); + var completionEndpoint = new RazorCompletionEndpoint(completionListProvider: null, triggerAndCommitCharacters: null, NoOpTelemetryReporter.Instance, optionsMonitor); var request = new CompletionParams() { TextDocument = new TextDocumentIdentifier() @@ -49,7 +50,7 @@ public async Task Handle_AutoShowCompletionDisabled_NoCompletionItems() var uri = new Uri(documentPath); var documentContext = CreateDocumentContext(uri, codeDocument); var optionsMonitor = GetOptionsMonitor(autoShowCompletion: false); - var completionEndpoint = new RazorCompletionEndpoint(completionListProvider: null, completionTriggerAndCommitCharacters: null, telemetryReporter: null, optionsMonitor); + var completionEndpoint = new RazorCompletionEndpoint(completionListProvider: null, triggerAndCommitCharacters: null, NoOpTelemetryReporter.Instance, optionsMonitor); var request = new CompletionParams() { TextDocument = new TextDocumentIdentifier() diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/RazorCompletionListProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/RazorCompletionListProviderTest.cs index 31e14e2fd1b..3beaadff6db 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/RazorCompletionListProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/RazorCompletionListProviderTest.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.Test.Common; @@ -378,18 +377,17 @@ public void TryConvert_TagHelperAttribute_ReturnsTrue() [InlineData("@page\r\n
\r\n@f$$\r\n")] [WorkItem("https://github.com/dotnet/razor-tooling/issues/4547")] [WorkItem("https://github.com/dotnet/razor/issues/9955")] - public async Task GetCompletionListAsync_ProvidesDirectiveCompletionItems(string documentText) + public void GetCompletionList_ProvidesDirectiveCompletionItems(string documentText) { // Arrange var documentPath = "C:/path/to/document.cshtml"; TestFileMarkupParser.GetPosition(documentText, out documentText, out var cursorPosition); - var codeDocument = CreateCodeDocument(documentText); - var documentContext = TestDocumentContext.Create(documentPath, codeDocument); + var codeDocument = CreateCodeDocument(documentText, documentPath); var provider = new RazorCompletionListProvider(_completionFactsService, _completionListCache, LoggerFactory); // Act - var completionList = await provider.GetCompletionListAsync( - absoluteIndex: cursorPosition, _defaultCompletionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); + var completionList = provider.GetCompletionList( + codeDocument, absoluteIndex: cursorPosition, _defaultCompletionContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions); // Assert @@ -402,22 +400,22 @@ public async Task GetCompletionListAsync_ProvidesDirectiveCompletionItems(string } [Fact] - public async Task GetCompletionListAsync_ProvidesDirectiveCompletions_IncompleteTriggerOnDeletion() + public void GetCompletionListAsync_ProvidesDirectiveCompletions_IncompleteTriggerOnDeletion() { // Arrange var documentPath = "C:/path/to/document.cshtml"; - var codeDocument = CreateCodeDocument("@"); - var documentContext = TestDocumentContext.Create(documentPath, codeDocument); + var codeDocument = CreateCodeDocument("@", documentPath); var completionContext = new VSInternalCompletionContext() { TriggerKind = CompletionTriggerKind.TriggerForIncompleteCompletions, InvokeKind = VSInternalCompletionInvokeKind.Deletion, }; + var provider = new RazorCompletionListProvider(_completionFactsService, _completionListCache, LoggerFactory); // Act - var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); + var completionList = provider.GetCompletionList( + codeDocument, absoluteIndex: 1, completionContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions); // Assert Assert.NotNull(completionList); @@ -430,7 +428,7 @@ public async Task GetCompletionListAsync_ProvidesDirectiveCompletions_Incomplete [Fact] [WorkItem("https://github.com/dotnet/razor-tooling/issues/4547")] - public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete_KeywordIn() + public void GetCompletionList_ProvidesInjectOnIncomplete_KeywordIn() { // Arrange var documentPath = "C:/path/to/document.razor"; @@ -439,9 +437,8 @@ public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete_KeywordIn() builder.Metadata(TypeName("TestNamespace.TestTagHelper")); var tagHelper = builder.Build(); var tagHelperContext = TagHelperDocumentContext.Create(prefix: string.Empty, [tagHelper]); - var codeDocument = CreateCodeDocument("@in"); + var codeDocument = CreateCodeDocument("@in", documentPath); codeDocument.SetTagHelperContext(tagHelperContext); - var documentContext = TestDocumentContext.Create(documentPath, codeDocument); var provider = new RazorCompletionListProvider(_completionFactsService, _completionListCache, LoggerFactory); var completionContext = new VSInternalCompletionContext() { @@ -449,8 +446,8 @@ public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete_KeywordIn() }; // Act - var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); + var completionList = provider.GetCompletionList( + codeDocument, absoluteIndex: 1, completionContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions); // Assert Assert.NotNull(completionList); @@ -461,7 +458,7 @@ public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete_KeywordIn() } [Fact] - public async Task GetCompletionListAsync_DoesNotProvideInjectOnInvoked() + public void GetCompletionList_DoesNotProvideInjectOnInvoked() { // Arrange var documentPath = "C:/path/to/document.razor"; @@ -470,9 +467,8 @@ public async Task GetCompletionListAsync_DoesNotProvideInjectOnInvoked() builder.Metadata(TypeName("TestNamespace.TestTagHelper")); var tagHelper = builder.Build(); var tagHelperContext = TagHelperDocumentContext.Create(prefix: string.Empty, [tagHelper]); - var codeDocument = CreateCodeDocument("@inje"); + var codeDocument = CreateCodeDocument("@inje", documentPath); codeDocument.SetTagHelperContext(tagHelperContext); - var documentContext = TestDocumentContext.Create(documentPath, codeDocument); var provider = new RazorCompletionListProvider(_completionFactsService, _completionListCache, LoggerFactory); var completionContext = new VSInternalCompletionContext() { @@ -480,8 +476,8 @@ public async Task GetCompletionListAsync_DoesNotProvideInjectOnInvoked() }; // Act - var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); + var completionList = provider.GetCompletionList( + codeDocument, absoluteIndex: 1, completionContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions); // Assert Assert.NotNull(completionList); @@ -490,7 +486,7 @@ public async Task GetCompletionListAsync_DoesNotProvideInjectOnInvoked() [Fact] [WorkItem("https://github.com/dotnet/razor-tooling/issues/4547")] - public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete() + public void GetCompletionList_ProvidesInjectOnIncomplete() { // Arrange var documentPath = "C:/path/to/document.razor"; @@ -499,9 +495,8 @@ public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete() builder.Metadata(TypeName("TestNamespace.TestTagHelper")); var tagHelper = builder.Build(); var tagHelperContext = TagHelperDocumentContext.Create(prefix: string.Empty, [tagHelper]); - var codeDocument = CreateCodeDocument("@inje"); + var codeDocument = CreateCodeDocument("@inje", documentPath); codeDocument.SetTagHelperContext(tagHelperContext); - var documentContext = TestDocumentContext.Create(documentPath, codeDocument); var provider = new RazorCompletionListProvider(_completionFactsService, _completionListCache, LoggerFactory); var completionContext = new VSInternalCompletionContext() { @@ -509,8 +504,8 @@ public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete() }; // Act - var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); + var completionList = provider.GetCompletionList( + codeDocument, absoluteIndex: 1, completionContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions); // Assert Assert.NotNull(completionList); @@ -522,7 +517,7 @@ public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete() // This is more of an integration test to validate that all the pieces work together [Fact] - public async Task GetCompletionListAsync_ProvidesTagHelperElementCompletionItems() + public void GetCompletionList_ProvidesTagHelperElementCompletionItems() { // Arrange var documentPath = "C:/path/to/document.cshtml"; @@ -531,14 +526,13 @@ public async Task GetCompletionListAsync_ProvidesTagHelperElementCompletionItems builder.Metadata(TypeName("TestNamespace.TestTagHelper")); var tagHelper = builder.Build(); var tagHelperContext = TagHelperDocumentContext.Create(prefix: string.Empty, [tagHelper]); - var codeDocument = CreateCodeDocument("<"); + var codeDocument = CreateCodeDocument("<", documentPath); codeDocument.SetTagHelperContext(tagHelperContext); - var documentContext = TestDocumentContext.Create(documentPath, codeDocument); var provider = new RazorCompletionListProvider(_completionFactsService, _completionListCache, LoggerFactory); // Act - var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, _defaultCompletionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); + var completionList = provider.GetCompletionList( + codeDocument, absoluteIndex: 1, _defaultCompletionContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions); // Assert Assert.NotNull(completionList); @@ -547,7 +541,7 @@ public async Task GetCompletionListAsync_ProvidesTagHelperElementCompletionItems // This is more of an integration test to validate that all the pieces work together [Fact] - public async Task GetCompletionListAsync_ProvidesTagHelperAttributeItems() + public void GetCompletionList_ProvidesTagHelperAttributeItems() { // Arrange var documentPath = "C:/path/to/document.cshtml"; @@ -562,14 +556,13 @@ public async Task GetCompletionListAsync_ProvidesTagHelperAttributeItems() builder.Metadata(TypeName("TestNamespace.TestTagHelper")); var tagHelper = builder.Build(); var tagHelperContext = TagHelperDocumentContext.Create(prefix: string.Empty, [tagHelper]); - var codeDocument = CreateCodeDocument(" item.InsertText == "testAttribute=$0"); } - private static RazorCodeDocument CreateCodeDocument(string text) + private static RazorCodeDocument CreateCodeDocument(string text, string documentFilePath) { var codeDocument = TestRazorCodeDocument.CreateEmpty(); - var sourceDocument = TestRazorSourceDocument.Create(text); + var sourceDocument = TestRazorSourceDocument.Create(text, filePath: documentFilePath); var syntaxTree = RazorSyntaxTree.Parse(sourceDocument); codeDocument.SetSyntaxTree(syntaxTree); var tagHelperDocumentContext = TagHelperDocumentContext.Create(prefix: string.Empty, tagHelpers: []);