Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -145,24 +147,20 @@ 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<VSInternalCompletionList?> GetCompletionListAsync(
public override ValueTask<VSInternalCompletionList?> GetCompletionListAsync(
RazorCodeDocument codeDocument,
int absoluteIndex,
VSInternalCompletionContext completionContext,
DocumentContext documentContext,
VSInternalClientCapabilities clientCapabilities,
RazorCompletionOptions completionOptions,
Guid correlationId,
CancellationToken cancellationToken)
{
return Task.FromResult<VSInternalCompletionList?>(
new VSInternalCompletionList
{
});
}
=> new(new VSInternalCompletionList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -22,7 +23,7 @@ internal class CompletionListProvider(
private readonly DelegatedCompletionListProvider _delegatedCompletionListProvider = delegatedCompletionListProvider;
private readonly CompletionTriggerAndCommitCharacters _triggerAndCommitCharacters = triggerAndCommitCharacters;

public async Task<VSInternalCompletionList?> GetCompletionListAsync(
public ValueTask<VSInternalCompletionList?> GetCompletionListAsync(
int absoluteIndex,
VSInternalCompletionContext completionContext,
DocumentContext documentContext,
Expand All @@ -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<VSInternalCompletionList?> 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<string>? existingItems = null;

// Extract the items we got back from the delegated server, to inform tag helper completion
var existingItems = delegatedCompletionList?.Items != null
? new HashSet<string>(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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public DelegatedCompletionListProvider(
}

// virtual for tests
public virtual async Task<VSInternalCompletionList?> GetCompletionListAsync(
public virtual ValueTask<VSInternalCompletionList?> GetCompletionListAsync(
RazorCodeDocument codeDocument,
int absoluteIndex,
VSInternalCompletionContext completionContext,
DocumentContext documentContext,
Expand All @@ -51,66 +52,78 @@ public DelegatedCompletionListProvider(
Guid correlationId,
CancellationToken cancellationToken)
{
var positionInfo = await _documentMappingService
.GetPositionInfoAsync(documentContext, absoluteIndex, cancellationToken)
.ConfigureAwait(false);

var positionInfo = _documentMappingService.GetPositionInfo(codeDocument, absoluteIndex);
if (positionInfo.LanguageKind == RazorLanguageKind.Razor)
{
// Nothing to delegate to.
return null;
return default;
}

var provisionalCompletion = await DelegatedCompletionHelper.TryGetProvisionalCompletionInfoAsync(
documentContext,
completionContext,
positionInfo,
_documentMappingService,
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)
{
return null;
return default;
}

completionContext = rewrittenContext;

var razorCodeDocument = 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(
return new(GetDelegatedCompletionListAsync(
codeDocument,
absoluteIndex,
completionContext,
documentContext.GetTextDocumentIdentifierAndVersion(),
Copy link
Member Author

Choose a reason for hiding this comment

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

ℹ️ Note that this is now the only reason a DocumentContext is passed through. Perhaps we can find a way to remove this in future.

clientCapabilities,
razorCompletionOptions,
correlationId,
positionInfo,
provisionalTextEdit,
shouldIncludeDelegationSnippets,
cancellationToken));
}

private async Task<VSInternalCompletionList?> 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,
provisionalTextEdit,
shouldIncludeDelegationSnippets,
correlationId);

var delegatedResponse = await _clientConnection.SendRequestAsync<DelegatedCompletionParams, VSInternalCompletionList?>(
LanguageServerConstants.RazorCompletionEndpointName,
delegatedParams,
cancellationToken).ConfigureAwait(false);

var rewrittenResponse = delegatedParams.ProjectedKind == RazorLanguageKind.CSharp
? await DelegatedCompletionHelper.RewriteCSharpResponseAsync(
delegatedResponse,
absoluteIndex,
documentContext,
delegatedParams.ProjectedPosition,
razorCompletionOptions,
var delegatedResponse = await _clientConnection
.SendRequestAsync<DelegatedCompletionParams, VSInternalCompletionList?>(
LanguageServerConstants.RazorCompletionEndpointName,
delegatedParams,
cancellationToken)
.ConfigureAwait(false)
.ConfigureAwait(false);

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion;
[RazorLanguageServerEndpoint(Methods.TextDocumentCompletionName)]
internal class RazorCompletionEndpoint(
CompletionListProvider completionListProvider,
CompletionTriggerAndCommitCharacters completionTriggerAndCommitCharacters,
ITelemetryReporter? telemetryReporter,
Copy link
Member Author

Choose a reason for hiding this comment

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

ℹ️ Only tests and benchmarks were calling the RazorCompletionEndpoint with a null telemetry reporter.

CompletionTriggerAndCommitCharacters triggerAndCommitCharacters,
ITelemetryReporter telemetryReporter,
RazorLSPOptionsMonitor optionsMonitor)
: IRazorRequestHandler<CompletionParams, VSInternalCompletionList?>, 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;
Expand All @@ -52,46 +52,43 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(CompletionParams request

public async Task<VSInternalCompletionList?> 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);
Copy link
Member Author

Choose a reason for hiding this comment

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

ℹ️ I've reordered some of the checks to push fast checks ahead of checks that require async work.

Copy link
Member

Choose a reason for hiding this comment

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

Do any of these changes apply to the cohost completion endpoint? Though even if they do, I'm fine if you want to leave that for me, I suspect I need to audit that, and compare to this endpoint anyway

Copy link
Member Author

@DustinCampbell DustinCampbell Mar 14, 2025

Choose a reason for hiding this comment

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

I didn't actually look at the co-hosting endpoint. I was primarily looking at callers of CompletionListProvider.GetCompletionListAsync(...). For co-hosting, that's RemoteCompletionService.

Copy link
Member

Choose a reason for hiding this comment

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

The options checking would be in the endpoint, which is why I thought of it, but I just looked and the ordering of the checks there is fine.

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);
}
}
}
Loading