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 @@ -34,7 +34,7 @@ internal class DelegatedCompletionItemResolver(
VSInternalCompletionItem item,
VSInternalCompletionList containingCompletionList,
ICompletionResolveContext originalRequestContext,
VSInternalClientCapabilities? clientCapabilities,
VSInternalClientCapabilities clientCapabilities,
IComponentAvailabilityService componentAvailabilityService,
CancellationToken cancellationToken)
{
Expand All @@ -54,7 +54,7 @@ internal class DelegatedCompletionItemResolver(

if (resolvedCompletionItem is not null)
{
resolvedCompletionItem = await PostProcessCompletionItemAsync(resolutionContext, resolvedCompletionItem, cancellationToken).ConfigureAwait(false);
resolvedCompletionItem = await PostProcessCompletionItemAsync(resolutionContext, resolvedCompletionItem, clientCapabilities, cancellationToken).ConfigureAwait(false);
}

return resolvedCompletionItem;
Expand All @@ -63,6 +63,7 @@ internal class DelegatedCompletionItemResolver(
private async Task<VSInternalCompletionItem> PostProcessCompletionItemAsync(
DelegatedCompletionResolutionContext context,
VSInternalCompletionItem resolvedCompletionItem,
VSInternalClientCapabilities clientCapabilities,
CancellationToken cancellationToken)
{
if (context.ProjectedKind != RazorLanguageKind.CSharp)
Expand All @@ -71,7 +72,7 @@ private async Task<VSInternalCompletionItem> PostProcessCompletionItemAsync(
return resolvedCompletionItem;
}

if (!resolvedCompletionItem.VsResolveTextEditOnCommit)
if (clientCapabilities.SupportsVisualStudioExtensions && !resolvedCompletionItem.VsResolveTextEditOnCommit)
{
// Resolve doesn't typically handle text edit resolution; however, in VS cases it does.
return resolvedCompletionItem;
Expand All @@ -89,12 +90,15 @@ private async Task<VSInternalCompletionItem> PostProcessCompletionItemAsync(
return resolvedCompletionItem;
}

var formattingOptions = await _clientConnection
.SendRequestAsync<TextDocumentIdentifierAndVersion, FormattingOptions?>(
LanguageServerConstants.RazorGetFormattingOptionsEndpointName,
documentContext.GetTextDocumentIdentifierAndVersion(),
cancellationToken)
.ConfigureAwait(false);
// In VS we call into the VS layer to get formatting options, as the editor decides based on a multiple sources
var formattingOptions = clientCapabilities.SupportsVisualStudioExtensions
? await _clientConnection
.SendRequestAsync<TextDocumentIdentifierAndVersion, FormattingOptions?>(
LanguageServerConstants.RazorGetFormattingOptionsEndpointName,
documentContext.GetTextDocumentIdentifierAndVersion(),
cancellationToken)
.ConfigureAwait(false)
: _optionsMonitor.CurrentValue.ToFormattingOptions();

if (formattingOptions is null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
using Microsoft.CodeAnalysis.Razor.Completion;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Tooltip;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion;
Expand All @@ -13,22 +14,17 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion;
internal class RazorCompletionResolveEndpoint(
AggregateCompletionItemResolver completionItemResolver,
CompletionListCache completionListCache,
IComponentAvailabilityService componentAvailabilityService)
: IRazorRequestHandler<VSInternalCompletionItem, VSInternalCompletionItem>, ICapabilitiesProvider
IComponentAvailabilityService componentAvailabilityService,
IClientCapabilitiesService clientCapabilitiesService)
: IRazorRequestHandler<VSInternalCompletionItem, VSInternalCompletionItem>
{
private readonly AggregateCompletionItemResolver _completionItemResolver = completionItemResolver;
private readonly CompletionListCache _completionListCache = completionListCache;
private readonly IComponentAvailabilityService _componentAvailabilityService = componentAvailabilityService;

private VSInternalClientCapabilities? _clientCapabilities;
private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService;

public bool MutatesSolutionState => false;

public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
{
_clientCapabilities = clientCapabilities;
}

public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalCompletionItem request)
{
var context = RazorCompletionResolveData.Unwrap(request);
Expand All @@ -50,7 +46,7 @@ public async Task<VSInternalCompletionItem> HandleRequestAsync(VSInternalComplet
completionItem,
containingCompletionList,
originalRequestContext,
_clientCapabilities,
_clientCapabilitiesService.ClientCapabilities,
_componentAvailabilityService,
cancellationToken)
.ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public static void AddCompletionServices(this IServiceCollection services)
{
services.AddHandlerWithCapabilities<InlineCompletionEndpoint>();
services.AddHandlerWithCapabilities<RazorCompletionEndpoint>();
services.AddHandlerWithCapabilities<RazorCompletionResolveEndpoint>();
services.AddHandler<RazorCompletionResolveEndpoint>();
services.AddSingleton<CompletionListCache>();
services.AddSingleton<CompletionListProvider>();
services.AddSingleton<DelegatedCompletionListProvider>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,13 @@ public override int GetHashCode()
hash.Add(TaskListDescriptors);
return hash;
}

internal FormattingOptions ToFormattingOptions()
{
return new FormattingOptions()
{
InsertSpaces = InsertSpaces,
TabSize = TabSize,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.PooledObjects;
Expand All @@ -13,26 +12,20 @@

namespace Microsoft.CodeAnalysis.Razor.Completion;

internal class AggregateCompletionItemResolver
internal class AggregateCompletionItemResolver(IEnumerable<CompletionItemResolver> completionItemResolvers, ILoggerFactory loggerFactory)
{
private readonly IReadOnlyList<CompletionItemResolver> _completionItemResolvers;
private readonly ILogger _logger;

public AggregateCompletionItemResolver(IEnumerable<CompletionItemResolver> completionItemResolvers, ILoggerFactory loggerFactory)
{
_completionItemResolvers = completionItemResolvers.ToArray();
_logger = loggerFactory.GetOrCreateLogger<AggregateCompletionItemResolver>();
}
private readonly ImmutableArray<CompletionItemResolver> _completionItemResolvers = [.. completionItemResolvers];
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<AggregateCompletionItemResolver>();

public async Task<VSInternalCompletionItem?> ResolveAsync(
VSInternalCompletionItem item,
VSInternalCompletionList containingCompletionList,
ICompletionResolveContext originalRequestContext,
VSInternalClientCapabilities? clientCapabilities,
VSInternalClientCapabilities clientCapabilities,
IComponentAvailabilityService componentAvailabilityService,
CancellationToken cancellationToken)
{
using var completionItemResolverTasks = new PooledArrayBuilder<Task<VSInternalCompletionItem?>>(_completionItemResolvers.Count);
using var completionItemResolverTasks = new PooledArrayBuilder<Task<VSInternalCompletionItem?>>(_completionItemResolvers.Length);

foreach (var completionItemResolver in _completionItemResolvers)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal abstract class CompletionItemResolver
VSInternalCompletionItem item,
VSInternalCompletionList containingCompletionList,
ICompletionResolveContext originalRequestContext,
VSInternalClientCapabilities? clientCapabilities,
VSInternalClientCapabilities clientCapabilities,
IComponentAvailabilityService componentAvailabilityService,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ public async Task<ImmutableArray<TextChange>> GetHtmlOnTypeFormattingChangesAsyn
triggerCharacter: '\0',
_csharpOnTypeFormattingPass,
collapseChanges: true,
automaticallyAddUsings: false,
automaticallyAddUsings: true,
validate: false,
cancellationToken: cancellationToken).ConfigureAwait(false);

Expand Down
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 @@ -18,6 +19,7 @@
using Microsoft.CodeAnalysis.Razor.Tooltip;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
Expand Down Expand Up @@ -214,6 +216,68 @@ async Task FooAsync()
Assert.True(expectedSourceText.ContentEquals(actualSourceText));
}

[Fact]
public async Task ResolveAsync_CSharp_RemapAndFormatsTextEdit_UsingDirective()
{
// Arrange
TestCode input =
"""
@{
Task FooAsync()
{
String$$
}
}
""";

// Admittedly the result here is not perfect, but only because our tests don't implement the full LSP editor logic. The key thing
// is the addition of the using directive.
var expectedSourceText = SourceText.From(
"""
@using System.Text
@{
Task FooAsync()
{
String
}
}
""");

var codeDocument = CreateCodeDocument(input.Text, filePath: "C:/path/to/file.razor");
// Roslyn won't send unimported types if SupportsVisualStudioExtensions is true
await using var csharpServer = await CreateCSharpServerAsync(codeDocument, supportsVisualStudioExtensions: false);

var clientConnection = CreateClientConnectionForResolve(csharpServer);
var documentContextFactory = new TestDocumentContextFactory("C:/path/to/file.razor", codeDocument);
var optionsMonitor = TestRazorLSPOptionsMonitor.Create();
var formattingService = await _lazyFormattingService.GetValueAsync(DisposalToken);
var resolver = new DelegatedCompletionItemResolver(documentContextFactory, formattingService, DocumentMappingService, optionsMonitor, clientConnection, LoggerFactory);
var containingCompletionList = await GetCompletionListAndOriginalParamsAsync(input.Position, codeDocument, csharpServer);

var sw = Stopwatch.StartNew();
VSInternalCompletionItem item;
while ((item = containingCompletionList.Items.FirstOrDefault(item => item.Label == "StringBuilder")) == null)
{
Assert.True(sw.Elapsed < TimeSpan.FromSeconds(5), "Failed to resolve unimported completion item after 5 second.");

// Roslyn only computes unimported types in the background, and we have no access to its internal workings to wait for it to be
// finished, so we just have to delay and ask for completion items again.
await Task.Delay(100, DisposalToken);
containingCompletionList = await GetCompletionListAndOriginalParamsAsync(input.Position, codeDocument, csharpServer);
}

Assert.NotNull(item);

var originalRequestContext = new DelegatedCompletionResolutionContext(_csharpCompletionParams.Identifier, _csharpCompletionParams.ProjectedKind, containingCompletionList.Data);
var resolvedItem = await resolver.ResolveAsync(
item, containingCompletionList, originalRequestContext, s_clientCapabilities, _componentAvailabilityService, DisposalToken);

var originalSourceText = SourceText.From(input.Text);
var textChange = originalSourceText.GetTextChange(resolvedItem.AdditionalTextEdits.Single());
var actualSourceText = originalSourceText.WithChanges(textChange);
AssertEx.EqualOrDiff(expectedSourceText.ToString(), actualSourceText.ToString());
}

[Fact]
public async Task ResolveAsync_Html_Resolves()
{
Expand Down Expand Up @@ -246,15 +310,14 @@ private async Task<VSInternalCompletionItem> ResolveCompletionItemAsync(string c
{
TestFileMarkupParser.GetPosition(content, out var documentContent, out var cursorPosition);
var codeDocument = CreateCodeDocument(documentContent, filePath: "C:/path/to/file.razor");
await using var csharpServer = await CreateCSharpServerAsync(codeDocument);
await using var csharpServer = await CreateCSharpServerAsync(codeDocument, supportsVisualStudioExtensions: true);

var clientConnection = CreateClientConnectionForResolve(csharpServer);
var documentContextFactory = new TestDocumentContextFactory("C:/path/to/file.razor", codeDocument);
var optionsMonitor = TestRazorLSPOptionsMonitor.Create();
var formattingService = await _lazyFormattingService.GetValueAsync(DisposalToken);
var resolver = new DelegatedCompletionItemResolver(documentContextFactory, formattingService, DocumentMappingService, optionsMonitor, clientConnection, LoggerFactory);
var (containingCompletionList, csharpCompletionParams) = await GetCompletionListAndOriginalParamsAsync(
cursorPosition, codeDocument, csharpServer);
var containingCompletionList = await GetCompletionListAndOriginalParamsAsync(cursorPosition, codeDocument, csharpServer);

var originalRequestContext = new DelegatedCompletionResolutionContext(_csharpCompletionParams.Identifier, _csharpCompletionParams.ProjectedKind, containingCompletionList.Data);
var item = containingCompletionList.Items.FirstOrDefault(item => item.Label == itemToResolve);
Expand All @@ -270,7 +333,7 @@ private async Task<VSInternalCompletionItem> ResolveCompletionItemAsync(string c
return resolvedItem;
}

private async Task<CSharpTestLspServer> CreateCSharpServerAsync(RazorCodeDocument codeDocument)
private async Task<CSharpTestLspServer> CreateCSharpServerAsync(RazorCodeDocument codeDocument, bool supportsVisualStudioExtensions)
{
var csharpSourceText = codeDocument.GetCSharpSourceText();
var csharpDocumentUri = new Uri("C:/path/to/file.razor__virtual.g.cs");
Expand All @@ -283,32 +346,33 @@ private async Task<CSharpTestLspServer> CreateCSharpServerAsync(RazorCodeDocumen
}
};

var capabilitiesUpdater = (VSInternalClientCapabilities c) =>
{
c.SupportsVisualStudioExtensions = supportsVisualStudioExtensions;
};

// Don't declare this with an 'await using'. The caller owns the lifetime of this C# LSP server.
var csharpServer = await CSharpTestLspServerHelpers.CreateCSharpLspServerAsync(
csharpSourceText, csharpDocumentUri, serverCapabilities, DisposalToken);
csharpSourceText, csharpDocumentUri, serverCapabilities, capabilitiesUpdater, DisposalToken);

await csharpServer.OpenDocumentAsync(csharpDocumentUri, csharpSourceText.ToString(), DisposalToken);

return csharpServer;
}

private async Task<(RazorVSInternalCompletionList, DelegatedCompletionParams)> GetCompletionListAndOriginalParamsAsync(
private async Task<RazorVSInternalCompletionList> GetCompletionListAndOriginalParamsAsync(
int cursorPosition,
RazorCodeDocument codeDocument,
CSharpTestLspServer csharpServer)
{
var completionContext = new VSInternalCompletionContext() { TriggerKind = CompletionTriggerKind.Invoked };
var documentContext = TestDocumentContext.Create("C:/path/to/file.razor", codeDocument);

DelegatedCompletionParams? delegatedParams = null;
var clientConnection = CreateClientConnectionForCompletion(csharpServer, processParams: @params =>
{
delegatedParams = @params;
});
var clientConnection = CreateClientConnectionForCompletion(csharpServer);

var provider = CreateDelegatedCompletionListProvider(clientConnection);

var completionList = await provider.GetCompletionListAsync(
return await provider.GetCompletionListAsync(
codeDocument,
cursorPosition,
completionContext,
Expand All @@ -317,7 +381,5 @@ private async Task<CSharpTestLspServer> CreateCSharpServerAsync(RazorCodeDocumen
s_defaultRazorCompletionOptions,
correlationId: Guid.Empty,
cancellationToken: DisposalToken);

return (completionList, delegatedParams);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.LanguageServer.Hover;
using Microsoft.AspNetCore.Razor.LanguageServer.Test;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.CodeAnalysis.Razor.Completion;
Expand All @@ -30,12 +31,6 @@ public RazorCompletionResolveEndpointTest(ITestOutputHelper testOutput)
var projectManager = CreateProjectSnapshotManager();
var componentAvailabilityService = new ComponentAvailabilityService(projectManager);

_endpoint = new RazorCompletionResolveEndpoint(
new AggregateCompletionItemResolver(
[new TestCompletionItemResolver()],
LoggerFactory),
_completionListCache,
componentAvailabilityService);
_clientCapabilities = new VSInternalClientCapabilities()
{
TextDocument = new TextDocumentClientCapabilities()
Expand All @@ -49,7 +44,15 @@ [new TestCompletionItemResolver()],
}
}
};
_endpoint.ApplyCapabilities(new(), _clientCapabilities);
var clientCapabilitiesService = new TestClientCapabilitiesService(_clientCapabilities);

_endpoint = new RazorCompletionResolveEndpoint(
new AggregateCompletionItemResolver(
[new TestCompletionItemResolver()],
LoggerFactory),
_completionListCache,
componentAvailabilityService,
clientCapabilitiesService);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ public static Task<CSharpTestLspServer> CreateCSharpLspServerAsync(
CancellationToken cancellationToken) =>
CreateCSharpLspServerAsync(csharpSourceText, csharpDocumentUri, serverCapabilities, new EmptyMappingService(), capabilitiesUpdater: null, cancellationToken);

public static Task<CSharpTestLspServer> CreateCSharpLspServerAsync(
SourceText csharpSourceText,
Uri csharpDocumentUri,
VSInternalServerCapabilities serverCapabilities,
Action<VSInternalClientCapabilities> capabilitiesUpdater,
CancellationToken cancellationToken) =>
CreateCSharpLspServerAsync(csharpSourceText, csharpDocumentUri, serverCapabilities, new EmptyMappingService(), capabilitiesUpdater, cancellationToken);

public static Task<CSharpTestLspServer> CreateCSharpLspServerAsync(
SourceText csharpSourceText,
Uri csharpDocumentUri,
Expand Down
Loading