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
@@ -1,15 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Completion;
using Microsoft.CodeAnalysis.Razor.Completion.Delegation;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Tooltip;
Expand Down Expand Up @@ -41,30 +38,13 @@ internal class DelegatedCompletionItemResolver(
return null;
}

var labelQuery = item.Label;
var associatedDelegatedCompletion = containingCompletionList.Items.FirstOrDefault(completion => string.Equals(labelQuery, completion.Label, StringComparison.Ordinal));
if (associatedDelegatedCompletion is null)
{
return null;
}

// If the data was merged to combine resultId with original data, undo that merge and set the data back
// to what it originally was for the delegated request
if (CompletionListMerger.TrySplit(associatedDelegatedCompletion.Data, out var splitData) && splitData.Length == 2)
{
item.Data = splitData[1];
}
else
{
item.Data = associatedDelegatedCompletion.Data ?? resolutionContext.OriginalCompletionListData;
}
item.Data = DelegatedCompletionHelper.GetOriginalCompletionItemData(item, containingCompletionList, resolutionContext.OriginalCompletionListData);

var delegatedParams = resolutionContext.OriginalRequestParams;
var delegatedResolveParams = new DelegatedCompletionItemResolveParams(
delegatedParams.Identifier,
resolutionContext.Identifier,
item,
delegatedParams.ProjectedKind);
var resolvedCompletionItem = await _clientConnection.SendRequestAsync<DelegatedCompletionItemResolveParams, VSInternalCompletionItem?>(CodeAnalysis.Razor.Protocol.LanguageServerConstants.RazorCompletionResolveEndpointName, delegatedResolveParams, cancellationToken).ConfigureAwait(false);
resolutionContext.ProjectedKind);
var resolvedCompletionItem = await _clientConnection.SendRequestAsync<DelegatedCompletionItemResolveParams, VSInternalCompletionItem?>(LanguageServerConstants.RazorCompletionResolveEndpointName, delegatedResolveParams, cancellationToken).ConfigureAwait(false);

if (resolvedCompletionItem is not null)
{
Expand All @@ -79,7 +59,7 @@ private async Task<VSInternalCompletionItem> PostProcessCompletionItemAsync(
VSInternalCompletionItem resolvedCompletionItem,
CancellationToken cancellationToken)
{
if (context.OriginalRequestParams.ProjectedKind != RazorLanguageKind.CSharp)
if (context.ProjectedKind != RazorLanguageKind.CSharp)
{
// We currently don't do any post-processing for non-C# items.
return resolvedCompletionItem;
Expand All @@ -97,7 +77,7 @@ private async Task<VSInternalCompletionItem> PostProcessCompletionItemAsync(
return resolvedCompletionItem;
}

var identifier = context.OriginalRequestParams.Identifier.TextDocumentIdentifier;
var identifier = context.Identifier.TextDocumentIdentifier;
if (!_documentContextFactory.TryCreate(identifier, out var documentContext))
{
return resolvedCompletionItem;
Expand All @@ -117,45 +97,6 @@ private async Task<VSInternalCompletionItem> PostProcessCompletionItemAsync(

var options = RazorFormattingOptions.From(formattingOptions, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);

var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
var csharpSourceText = await documentContext.GetCSharpSourceTextAsync(cancellationToken).ConfigureAwait(false);

if (resolvedCompletionItem.TextEdit is not null)
{
if (resolvedCompletionItem.TextEdit.Value.TryGetFirst(out var textEdit))
{
var textChange = csharpSourceText.GetTextChange(textEdit);
var formattedTextChange = await _formattingService.TryGetCSharpSnippetFormattingEditAsync(
documentContext,
[textChange],
options,
cancellationToken).ConfigureAwait(false);

if (formattedTextChange is { } change)
{
resolvedCompletionItem.TextEdit = sourceText.GetTextEdit(change);
}
}
else
{
// TO-DO: Handle InsertReplaceEdit type
// https://github.com/dotnet/razor/issues/8829
Debug.Fail("Unsupported edit type.");
}
}

if (resolvedCompletionItem.AdditionalTextEdits is not null)
{
var additionalChanges = resolvedCompletionItem.AdditionalTextEdits.SelectAsArray(csharpSourceText.GetTextChange);
var formattedTextChange = await _formattingService.TryGetCSharpSnippetFormattingEditAsync(
documentContext,
additionalChanges,
options,
cancellationToken).ConfigureAwait(false);

resolvedCompletionItem.AdditionalTextEdits = formattedTextChange is { } change ? [sourceText.GetTextEdit(change)] : null;
}

return resolvedCompletionItem;
return await DelegatedCompletionHelper.FormatCSharpCompletionItemAsync(resolvedCompletionItem, documentContext, options, _formattingService, cancellationToken).ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,16 @@

namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation;

internal class DelegatedCompletionListProvider
internal class DelegatedCompletionListProvider(
IDocumentMappingService documentMappingService,
IClientConnection clientConnection,
CompletionListCache completionListCache,
CompletionTriggerAndCommitCharacters completionTriggerAndCommitCharacters)
{
private readonly IDocumentMappingService _documentMappingService;
private readonly IClientConnection _clientConnection;
private readonly CompletionListCache _completionListCache;
private readonly CompletionTriggerAndCommitCharacters _triggerAndCommitCharacters;

public DelegatedCompletionListProvider(
IDocumentMappingService documentMappingService,
IClientConnection clientConnection,
CompletionListCache completionListCache,
CompletionTriggerAndCommitCharacters completionTriggerAndCommitCharacters)
{
_documentMappingService = documentMappingService;
_clientConnection = clientConnection;
_completionListCache = completionListCache;
_triggerAndCommitCharacters = completionTriggerAndCommitCharacters;
}
private readonly IDocumentMappingService _documentMappingService = documentMappingService;
private readonly IClientConnection _clientConnection = clientConnection;
private readonly CompletionListCache _completionListCache = completionListCache;
private readonly CompletionTriggerAndCommitCharacters _triggerAndCommitCharacters = completionTriggerAndCommitCharacters;

// virtual for tests
public virtual ValueTask<RazorVSInternalCompletionList?> GetCompletionListAsync(
Expand Down Expand Up @@ -125,7 +117,7 @@ public DelegatedCompletionListProvider(
: DelegatedCompletionHelper.RewriteHtmlResponse(delegatedResponse, razorCompletionOptions);

var completionCapability = clientCapabilities?.TextDocument?.Completion as VSInternalCompletionSetting;
var resolutionContext = new DelegatedCompletionResolutionContext(delegatedParams, rewrittenResponse.Data);
var resolutionContext = new DelegatedCompletionResolutionContext(identifier, positionInfo.LanguageKind, rewrittenResponse.Data);
var resultId = _completionListCache.Add(rewrittenResponse, resolutionContext);
rewrittenResponse.SetResultId(resultId, completionCapability);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(CompletionParams request
AutoInsertAttributeQuotes: options.AutoInsertAttributeQuotes,
CommitElementsWithSpace: options.CommitElementsWithSpace);

return await _completionListProvider
var result = await _completionListProvider
.GetCompletionListAsync(
hostDocumentIndex,
completionContext,
Expand All @@ -87,6 +87,17 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(CompletionParams request
correlationId,
cancellationToken)
.ConfigureAwait(false);

if (result is null)
{
return null;
}

var completionCapability = _clientCapabilities?.TextDocument?.Completion as VSInternalCompletionSetting;
var supportsCompletionListData = completionCapability?.CompletionList?.Data ?? false;

RazorCompletionResolveData.Wrap(result, request.TextDocument, supportsCompletionListData: supportsCompletionListData);
return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
Expand All @@ -16,7 +14,7 @@ internal class RazorCompletionResolveEndpoint(
AggregateCompletionItemResolver completionItemResolver,
CompletionListCache completionListCache,
IComponentAvailabilityService componentAvailabilityService)
: IRazorDocumentlessRequestHandler<VSInternalCompletionItem, VSInternalCompletionItem>, ICapabilitiesProvider
: IRazorRequestHandler<VSInternalCompletionItem, VSInternalCompletionItem>, ICapabilitiesProvider
{
private readonly AggregateCompletionItemResolver _completionItemResolver = completionItemResolver;
private readonly CompletionListCache _completionListCache = completionListCache;
Expand All @@ -31,9 +29,18 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V
_clientCapabilities = clientCapabilities;
}

public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalCompletionItem request)
{
var context = RazorCompletionResolveData.Unwrap(request);
return context.TextDocument;
}

public async Task<VSInternalCompletionItem> HandleRequestAsync(VSInternalCompletionItem completionItem, RazorRequestContext requestContext, CancellationToken cancellationToken)
{
if (!TryGetOriginalRequestData(completionItem, out var containingCompletionList, out var originalRequestContext))
var data = RazorCompletionResolveData.Unwrap(completionItem);
completionItem.Data = data.OriginalData;

if (!_completionListCache.TryGetOriginalRequestData(completionItem, out var containingCompletionList, out var originalRequestContext))
{
return completionItem;
}
Expand All @@ -52,33 +59,4 @@ public async Task<VSInternalCompletionItem> HandleRequestAsync(VSInternalComplet

return resolvedCompletionItem;
}

private bool TryGetOriginalRequestData(VSInternalCompletionItem completionItem, [NotNullWhen(true)] out VSInternalCompletionList? completionList, [NotNullWhen(true)] out ICompletionResolveContext? context)
{
context = null;
completionList = null;

if (!completionItem.TryGetCompletionListResultIds(out var resultIds))
{
// Unable to lookup completion item result info
return false;
}

foreach (var resultId in resultIds)
{
// See if this is the right completion list for this corresponding completion item. We cross-check this based on label only given that
// is what users interact with.
if (_completionListCache.TryGet(resultId, out completionList, out context) &&
completionList.Items.Any(
completion =>
completionItem.Label == completion.Label &&
// Check the Kind as well, e.g. we may have a Razor snippet and a C# keyword with the same label, etc.
completionItem.Kind == completion.Kind))
{
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Diagnostics.CodeAnalysis;
using System.Linq;

namespace Microsoft.CodeAnalysis.Razor.Completion;

Expand Down Expand Up @@ -42,7 +43,7 @@ public int Add(VSInternalCompletionList completionList, ICompletionResolveContex
}
}

public bool TryGet(int id, [NotNullWhen(true)] out VSInternalCompletionList? completionList, [NotNullWhen(true)] out ICompletionResolveContext? context)
private bool TryGet(int id, [NotNullWhen(true)] out VSInternalCompletionList? completionList, [NotNullWhen(true)] out ICompletionResolveContext? context)
{
lock (_accessLock)
{
Expand Down Expand Up @@ -88,4 +89,34 @@ public bool TryGet(int id, [NotNullWhen(true)] out VSInternalCompletionList? com
return false;
}
}

public bool TryGetOriginalRequestData(VSInternalCompletionItem completionItem, [NotNullWhen(true)] out VSInternalCompletionList? completionList, [NotNullWhen(true)] out ICompletionResolveContext? context)
{
context = null;
completionList = null;

if (!completionItem.TryGetCompletionListResultIds(out var resultIds))
{
// Unable to lookup completion item result info
return false;
}

foreach (var resultId in resultIds)
{
if (TryGet(resultId, out completionList, out context) &&
// See if this is the right completion list for this corresponding completion item. We cross-check this based on label only given that
// is what users interact with.
completionList.Items.Any(
completion =>
completionItem.Label == completion.Label &&
// Check the Kind as well, e.g. we may have a Razor snippet and a C# keyword with the same label, etc.
completionItem.Kind == completion.Kind))
{
return true;
}
}

// Unable to lookup completion item result info
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) .NET Foundation. All rights reserved.

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Generic;
Expand All @@ -7,14 +8,15 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Razor.PooledObjects;

namespace Microsoft.CodeAnalysis.Razor.Completion;

internal static class CompletionListMerger
{
private static readonly string s_data1Key = nameof(MergedCompletionListData.Data1);
private static readonly string s_data2Key = nameof(MergedCompletionListData.Data2);
private const string Data1Key = nameof(MergedCompletionListData.Data1);
private const string Data2Key = nameof(MergedCompletionListData.Data2);
private static readonly object s_emptyData = new();

[return: NotNullIfNotNull(nameof(razorCompletionList))]
Expand Down Expand Up @@ -86,6 +88,14 @@ public static bool TrySplit(object? data, out ImmutableArray<JsonElement> splitD
return false;
}

// Needed for tests. We shouldn't ever have RazorCompletionResolveData leak out, but in our tests we avoid some
// serialization boundaries, like between devenv and OOP. In production not only should it never happen, but
// if it did, the type of Data would be JsonElement, so we wouldn't fall into this branch anyway.
if (data is RazorCompletionResolveData { OriginalData: var originalData })
{
return TrySplit(originalData, out splitData);
}

using var collector = new PooledArrayBuilder<JsonElement>();
Split(data, ref collector.AsRef());

Expand Down Expand Up @@ -120,8 +130,7 @@ private static void TrySplitJsonElement(object data, ref PooledArrayBuilder<Json
return;
}

if ((jsonElement.TryGetProperty(s_data1Key, out _) || jsonElement.TryGetProperty(s_data1Key.ToLowerInvariant(), out _)) &&
(jsonElement.TryGetProperty(s_data2Key, out _) || jsonElement.TryGetProperty(s_data2Key.ToLowerInvariant(), out _)))
if (jsonElement.TryGetProperty(Data1Key, out _) && jsonElement.TryGetProperty(Data2Key, out _))
{
// Merged data
var mergedCompletionListData = jsonElement.Deserialize<MergedCompletionListData>();
Expand Down Expand Up @@ -225,5 +234,7 @@ completionItem.CommitCharacters is not null ||
return inheritableCompletions.ToImmutable();
}

private record MergedCompletionListData(object Data1, object Data2);
private record MergedCompletionListData(
[property: JsonPropertyName(Data1Key)] object Data1,
[property: JsonPropertyName(Data2Key)] object Data2);
}
Loading