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,7 +7,9 @@
using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Completion;
using Microsoft.CodeAnalysis.Razor.Completion.Delegation;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Tooltip;

Expand All @@ -16,13 +18,17 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation;
internal class DelegatedCompletionItemResolver(
IDocumentContextFactory documentContextFactory,
IRazorFormattingService formattingService,
IDocumentMappingService documentMappingService,
RazorLSPOptionsMonitor optionsMonitor,
IClientConnection clientConnection) : CompletionItemResolver
IClientConnection clientConnection,
ILoggerFactory loggerFactory) : CompletionItemResolver
{
private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory;
private readonly IRazorFormattingService _formattingService = formattingService;
private readonly IDocumentMappingService _documentMappingService = documentMappingService;
private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor;
private readonly IClientConnection _clientConnection = clientConnection;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<DelegatedCompletionItemResolver>();

public override async Task<VSInternalCompletionItem?> ResolveAsync(
VSInternalCompletionItem item,
Expand Down Expand Up @@ -97,6 +103,6 @@ private async Task<VSInternalCompletionItem> PostProcessCompletionItemAsync(

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

return await DelegatedCompletionHelper.FormatCSharpCompletionItemAsync(resolvedCompletionItem, documentContext, options, _formattingService, cancellationToken).ConfigureAwait(false);
return await DelegatedCompletionHelper.FormatCSharpCompletionItemAsync(resolvedCompletionItem, documentContext, options, _formattingService, _documentMappingService, _logger, cancellationToken).ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ internal class DelegatedCompletionListProvider(
: DelegatedCompletionHelper.RewriteHtmlResponse(delegatedResponse, razorCompletionOptions);

var completionCapability = clientCapabilities?.TextDocument?.Completion as VSInternalCompletionSetting;
var resolutionContext = new DelegatedCompletionResolutionContext(identifier, positionInfo.LanguageKind, rewrittenResponse.Data);
var resolutionContext = new DelegatedCompletionResolutionContext(identifier, positionInfo.LanguageKind, rewrittenResponse.Data ?? rewrittenResponse.ItemDefaults?.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 @@ -94,7 +94,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(CompletionParams request
}

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

RazorCompletionResolveData.Wrap(result, request.TextDocument, supportsCompletionListData: supportsCompletionListData);
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public ImmutableArray<Registration> GetRegistrations(VSInternalClientCapabilitie
Method = Methods.TextDocumentCompletionName,
RegisterOptions = new CompletionRegistrationOptions()
{
ResolveProvider = false, // TODO - change to true when Resolve is implemented
ResolveProvider = true,
TriggerCharacters = [.. _triggerAndCommitCharacters.AllTriggerCharacters],
AllCommitCharacters = [.. _triggerAndCommitCharacters.AllCommitCharacters]
}
Expand Down Expand Up @@ -212,7 +212,7 @@ public ImmutableArray<Registration> GetRegistrations(VSInternalClientCapabilitie
}

var completionCapability = _clientCapabilitiesService.ClientCapabilities.TextDocument?.Completion as VSInternalCompletionSetting;
var supportsCompletionListData = completionCapability?.CompletionList?.Data ?? false;
var supportsCompletionListData = completionCapability.SupportsCompletionListData();

RazorCompletionResolveData.Wrap(combinedCompletionList, originalTextDocumentIdentifier, supportsCompletionListData: supportsCompletionListData);

Expand All @@ -239,7 +239,7 @@ public ImmutableArray<Registration> GetRegistrations(VSInternalClientCapabilitie
var completionCapability = _clientCapabilitiesService.ClientCapabilities.TextDocument?.Completion as VSInternalCompletionSetting;

var razorDocumentIdentifier = new TextDocumentIdentifierAndVersion(request.TextDocument, Version: 0);
var resolutionContext = new DelegatedCompletionResolutionContext(razorDocumentIdentifier, RazorLanguageKind.Html, rewrittenResponse.Data);
var resolutionContext = new DelegatedCompletionResolutionContext(razorDocumentIdentifier, RazorLanguageKind.Html, rewrittenResponse.Data ?? rewrittenResponse.ItemDefaults?.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 @@ -44,7 +44,7 @@ internal sealed class CohostDocumentCompletionResolveEndpoint(
private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager;
private readonly IHtmlRequestInvoker _requestInvoker = requestInvoker;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CohostDocumentCompletionEndpoint>();
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CohostDocumentCompletionResolveEndpoint>();

protected override bool MutatesSolutionState => false;

Expand Down Expand Up @@ -103,7 +103,7 @@ public ImmutableArray<Registration> GetRegistrations(VSInternalClientCapabilitie
Debug.Assert(_logger is not null);
Debug.Assert(nameof(DelegatedCompletionHelper).Length > 0);

// We don't support completion resolve in VS Code
// We don't support Html completion resolve in VS Code
return completionItem;
#else
completionItem.Data = DelegatedCompletionHelper.GetOriginalCompletionItemData(completionItem, completionList, delegatedContext.OriginalCompletionListData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ internal static class CompletionListMerger
// We don't fully support merging continue characters currently. Razor doesn't currently use them so delegated completion lists always win.
var mergedContinueWithCharacters = razorCompletionList.ContinueCharacters ?? delegatedCompletionList.ContinueCharacters;

var mergedItemDefaultsData = MergeData(razorCompletionList.ItemDefaults?.Data, delegatedCompletionList.ItemDefaults?.Data);
// We don't fully support merging edit ranges currently. Razor doesn't currently use them so delegated completion lists always win.
var mergedItemDefaultsEditRange = razorCompletionList.ItemDefaults?.EditRange ?? delegatedCompletionList.ItemDefaults?.EditRange;

Expand All @@ -58,6 +59,7 @@ internal static class CompletionListMerger
ContinueCharacters = mergedContinueWithCharacters,
ItemDefaults = new CompletionListItemDefaults()
{
Data = mergedItemDefaultsData,
EditRange = mergedItemDefaultsEditRange,
}
};
Expand Down Expand Up @@ -152,14 +154,16 @@ private static void TrySplitJsonElement(object data, ref PooledArrayBuilder<Json

private static void EnsureMergeableData(RazorVSInternalCompletionList completionListA, RazorVSInternalCompletionList completionListB)
{
if (completionListA.Data != completionListB.Data &&
(completionListA.Data is null || completionListB.Data is null))
var completionListAData = completionListA.Data ?? completionListA.ItemDefaults?.Data;
var completionListBData = completionListB.Data ?? completionListB.ItemDefaults?.Data;
if (completionListAData != completionListBData &&
(completionListAData is null || completionListBData is null))
{
// One of the completion lists have data while the other does not, we need to ensure that any non-data centric items don't get incorrect data associated

// The candidate completion list will be one where we populate empty data for any `null` specifying data given we'll be merging
// two completion lists together we don't want incorrect data to be inherited down
var candidateCompletionList = completionListA.Data is null ? completionListA : completionListB;
var candidateCompletionList = completionListAData is null ? completionListA : completionListB;
for (var i = 0; i < candidateCompletionList.Items.Length; i++)
{
var item = candidateCompletionList.Items[i];
Expand Down Expand Up @@ -206,11 +210,7 @@ private static void EnsureMergeableCommitCharacters(RazorVSInternalCompletionLis
}

completionListToStopInheriting.CommitCharacters = null;

if (completionListToStopInheriting.ItemDefaults is not null)
{
completionListToStopInheriting.ItemDefaults.CommitCharacters = null;
}
completionListToStopInheriting.ItemDefaults?.CommitCharacters = null;
}
}

Expand All @@ -219,8 +219,7 @@ private static ImmutableArray<VSInternalCompletionItem> GetCompletionsThatDoNotS
using var inheritableCompletions = new PooledArrayBuilder<VSInternalCompletionItem>();
for (var i = 0; i < completionList.Items.Length; i++)
{
var completionItem = completionList.Items[i] as VSInternalCompletionItem;
if (completionItem is null ||
if (completionList.Items[i] is not VSInternalCompletionItem completionItem ||
completionItem.CommitCharacters is not null ||
completionItem.VsCommitCharacters is not null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Protocol.Completion;
Expand Down Expand Up @@ -273,25 +274,60 @@ public static bool ShouldIncludeSnippets(RazorCodeDocument razorCodeDocument, in
return originalData;
}

public static async Task<VSInternalCompletionItem> FormatCSharpCompletionItemAsync(VSInternalCompletionItem resolvedCompletionItem, DocumentContext documentContext, RazorFormattingOptions options, IRazorFormattingService formattingService, CancellationToken cancellationToken)
public static async Task<VSInternalCompletionItem> FormatCSharpCompletionItemAsync(
VSInternalCompletionItem resolvedCompletionItem,
DocumentContext documentContext,
RazorFormattingOptions options,
IRazorFormattingService formattingService,
IDocumentMappingService documentMappingService,
ILogger logger,
CancellationToken cancellationToken)
{
var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
var csharpSourceText = await documentContext.GetCSharpSourceTextAsync(cancellationToken).ConfigureAwait(false);
// In VS Code, Roslyn does resolve via a custom command. Thats fine, but we have to modify the text edit sitting within it,
// rather than the one LSP knows about.
if (resolvedCompletionItem.Command is { CommandIdentifier: "roslyn.client.completionComplexEdit", Arguments: var args })
{
if (args is [TextDocumentIdentifier, TextEdit complexEdit, _, int nextCursorPosition])
{
var formattedTextEdit = await FormatTextEditsAsync([complexEdit], documentContext, options, formattingService, cancellationToken).ConfigureAwait(false);
if (formattedTextEdit is null)
{
resolvedCompletionItem.Command = null;
}
else
{
args[0] = documentContext.GetTextDocumentIdentifier();
args[1] = formattedTextEdit;
if (nextCursorPosition >= 0)
{
// nextCursorPosition is where VS Code will navigate to, so we translate it to our document, or set to 0 to do nothing.
var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
args[3] = documentMappingService.TryMapToHostDocumentPosition(codeDocument.GetCSharpDocument(), nextCursorPosition, out _, out nextCursorPosition)
? nextCursorPosition
: 0;
}
}
}
else
{
logger.LogError($"Unexpected arguments for command '{resolvedCompletionItem.Command.CommandIdentifier}': Expected: [TextDocumentIdentifier, TextEdit, _, int], Actual: {GetArgumentTypesLogString(resolvedCompletionItem)}");
Debug.Fail("Unexpected arguments for Roslyn complex edit command. Have they changed things?");
}
}
else if (resolvedCompletionItem.Command is not null)
{
logger.LogError($"Unsupported command for Razor document: {resolvedCompletionItem.Command.CommandIdentifier}.");
Debug.Fail("Unexpected command. Do we need to add something to support a new feature?");
}

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)
var formattedTextChange = await FormatTextEditsAsync([textEdit], documentContext, options, formattingService, cancellationToken).ConfigureAwait(false);
if (formattedTextChange is not null)
{
resolvedCompletionItem.TextEdit = sourceText.GetTextEdit(change);
resolvedCompletionItem.TextEdit = formattedTextChange;
}
}
else
Expand All @@ -304,16 +340,35 @@ public static async Task<VSInternalCompletionItem> FormatCSharpCompletionItemAsy

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;
var formattedTextChange = await FormatTextEditsAsync(resolvedCompletionItem.AdditionalTextEdits, documentContext, options, formattingService, cancellationToken).ConfigureAwait(false);
resolvedCompletionItem.AdditionalTextEdits = formattedTextChange is { } change ? [change] : null;
}

return resolvedCompletionItem;

static string GetArgumentTypesLogString(VSInternalCompletionItem resolvedCompletionItem)
{
if (resolvedCompletionItem.Command?.Arguments is { } args)
{
return "[" + string.Join(", ", args.Select(a => a.GetType().Name)) + "]";
}

return "null";
}
}

private static async Task<TextEdit?> FormatTextEditsAsync(TextEdit[] textEdits, DocumentContext documentContext, RazorFormattingOptions options, IRazorFormattingService formattingService, CancellationToken cancellationToken)
{
var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
var csharpSourceText = await documentContext.GetCSharpSourceTextAsync(cancellationToken).ConfigureAwait(false);

var changes = textEdits.SelectAsArray(csharpSourceText.GetTextChange);
var formattedTextChange = await formattingService.TryGetCSharpSnippetFormattingEditAsync(
documentContext,
changes,
options,
cancellationToken).ConfigureAwait(false);

return formattedTextChange is { } change ? sourceText.GetTextEdit(change) : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ internal class RazorCompletionListProvider(

_logger.LogTrace($"Resolved {razorCompletionItems.Length} completion items.");

// No point caching or setting data for an empty completion list.
if (razorCompletionItems.Length == 0)
{
return null;
}

var completionList = CreateLSPCompletionList(razorCompletionItems, clientCapabilities);

var completionCapability = clientCapabilities?.TextDocument?.Completion as VSInternalCompletionSetting;
Expand Down Expand Up @@ -115,10 +121,7 @@ internal static bool TryConvert(
VSInternalClientCapabilities clientCapabilities,
[NotNullWhen(true)] out VSInternalCompletionItem? completionItem)
{
if (razorCompletionItem is null)
{
throw new ArgumentNullException(nameof(razorCompletionItem));
}
ArgHelper.ThrowIfNull(razorCompletionItem);

var tagHelperCompletionItemKind = CompletionItemKind.TypeParameter;
var supportedItemKinds = clientCapabilities.TextDocument?.Completion?.CompletionItemKind?.ValueSet ?? [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ public static RazorCompletionResolveData Unwrap(CompletionItem completionItem)
throw new InvalidOperationException($"Invalid completion item received'{completionItem.Label}'.");
}

var context = paramsObj.Deserialize<RazorCompletionResolveData>();
if (context is null)
if (paramsObj.Deserialize<RazorCompletionResolveData>() is not { } context)
{
throw new InvalidOperationException($"completionItem.Data should be convertible to {nameof(RazorCompletionResolveData)}");
}
Expand All @@ -37,8 +36,17 @@ public static void Wrap(VSInternalCompletionList completionList, TextDocumentIde

if (supportsCompletionListData)
{
// Can set data at the completion list level
completionList.Data = data with { OriginalData = completionList.Data };
if (completionList.Data is not null)
{
// Can set data at the completion list level
completionList.Data = data with { OriginalData = completionList.Data };
}

if (completionList.ItemDefaults?.Data is not null)
{
// Set data for the item defaults
completionList.ItemDefaults.Data = data with { OriginalData = completionList.ItemDefaults.Data };
}

// Set data for items that won't inherit the default
foreach (var completionItem in completionList.Items.Where(static c => c.Data is not null))
Expand Down
Loading
Loading