Skip to content

Commit 2b049e1

Browse files
authored
Allow completion items to add using directives (#12034)
2 parents 88d10f2 + b2b42ca commit 2b049e1

File tree

11 files changed

+133
-56
lines changed

11 files changed

+133
-56
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionItemResolver.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ internal class DelegatedCompletionItemResolver(
3434
VSInternalCompletionItem item,
3535
VSInternalCompletionList containingCompletionList,
3636
ICompletionResolveContext originalRequestContext,
37-
VSInternalClientCapabilities? clientCapabilities,
37+
VSInternalClientCapabilities clientCapabilities,
3838
IComponentAvailabilityService componentAvailabilityService,
3939
CancellationToken cancellationToken)
4040
{
@@ -54,7 +54,7 @@ internal class DelegatedCompletionItemResolver(
5454

5555
if (resolvedCompletionItem is not null)
5656
{
57-
resolvedCompletionItem = await PostProcessCompletionItemAsync(resolutionContext, resolvedCompletionItem, cancellationToken).ConfigureAwait(false);
57+
resolvedCompletionItem = await PostProcessCompletionItemAsync(resolutionContext, resolvedCompletionItem, clientCapabilities, cancellationToken).ConfigureAwait(false);
5858
}
5959

6060
return resolvedCompletionItem;
@@ -63,6 +63,7 @@ internal class DelegatedCompletionItemResolver(
6363
private async Task<VSInternalCompletionItem> PostProcessCompletionItemAsync(
6464
DelegatedCompletionResolutionContext context,
6565
VSInternalCompletionItem resolvedCompletionItem,
66+
VSInternalClientCapabilities clientCapabilities,
6667
CancellationToken cancellationToken)
6768
{
6869
if (context.ProjectedKind != RazorLanguageKind.CSharp)
@@ -71,7 +72,7 @@ private async Task<VSInternalCompletionItem> PostProcessCompletionItemAsync(
7172
return resolvedCompletionItem;
7273
}
7374

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

92-
var formattingOptions = await _clientConnection
93-
.SendRequestAsync<TextDocumentIdentifierAndVersion, FormattingOptions?>(
94-
LanguageServerConstants.RazorGetFormattingOptionsEndpointName,
95-
documentContext.GetTextDocumentIdentifierAndVersion(),
96-
cancellationToken)
97-
.ConfigureAwait(false);
93+
// In VS we call into the VS layer to get formatting options, as the editor decides based on a multiple sources
94+
var formattingOptions = clientCapabilities.SupportsVisualStudioExtensions
95+
? await _clientConnection
96+
.SendRequestAsync<TextDocumentIdentifierAndVersion, FormattingOptions?>(
97+
LanguageServerConstants.RazorGetFormattingOptionsEndpointName,
98+
documentContext.GetTextDocumentIdentifierAndVersion(),
99+
cancellationToken)
100+
.ConfigureAwait(false)
101+
: _optionsMonitor.CurrentValue.ToFormattingOptions();
98102

99103
if (formattingOptions is null)
100104
{

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionResolveEndpoint.cs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading.Tasks;
66
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
77
using Microsoft.CodeAnalysis.Razor.Completion;
8+
using Microsoft.CodeAnalysis.Razor.Protocol;
89
using Microsoft.CodeAnalysis.Razor.Tooltip;
910

1011
namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion;
@@ -13,22 +14,17 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion;
1314
internal class RazorCompletionResolveEndpoint(
1415
AggregateCompletionItemResolver completionItemResolver,
1516
CompletionListCache completionListCache,
16-
IComponentAvailabilityService componentAvailabilityService)
17-
: IRazorRequestHandler<VSInternalCompletionItem, VSInternalCompletionItem>, ICapabilitiesProvider
17+
IComponentAvailabilityService componentAvailabilityService,
18+
IClientCapabilitiesService clientCapabilitiesService)
19+
: IRazorRequestHandler<VSInternalCompletionItem, VSInternalCompletionItem>
1820
{
1921
private readonly AggregateCompletionItemResolver _completionItemResolver = completionItemResolver;
2022
private readonly CompletionListCache _completionListCache = completionListCache;
2123
private readonly IComponentAvailabilityService _componentAvailabilityService = componentAvailabilityService;
22-
23-
private VSInternalClientCapabilities? _clientCapabilities;
24+
private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService;
2425

2526
public bool MutatesSolutionState => false;
2627

27-
public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
28-
{
29-
_clientCapabilities = clientCapabilities;
30-
}
31-
3228
public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalCompletionItem request)
3329
{
3430
var context = RazorCompletionResolveData.Unwrap(request);
@@ -50,7 +46,7 @@ public async Task<VSInternalCompletionItem> HandleRequestAsync(VSInternalComplet
5046
completionItem,
5147
containingCompletionList,
5248
originalRequestContext,
53-
_clientCapabilities,
49+
_clientCapabilitiesService.ClientCapabilities,
5450
_componentAvailabilityService,
5551
cancellationToken)
5652
.ConfigureAwait(false);

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public static void AddCompletionServices(this IServiceCollection services)
7474
{
7575
services.AddHandlerWithCapabilities<InlineCompletionEndpoint>();
7676
services.AddHandlerWithCapabilities<RazorCompletionEndpoint>();
77-
services.AddHandlerWithCapabilities<RazorCompletionResolveEndpoint>();
77+
services.AddHandler<RazorCompletionResolveEndpoint>();
7878
services.AddSingleton<CompletionListCache>();
7979
services.AddSingleton<CompletionListProvider>();
8080
services.AddSingleton<DelegatedCompletionListProvider>();

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,13 @@ public override int GetHashCode()
106106
hash.Add(TaskListDescriptors);
107107
return hash;
108108
}
109+
110+
internal FormattingOptions ToFormattingOptions()
111+
{
112+
return new FormattingOptions()
113+
{
114+
InsertSpaces = InsertSpaces,
115+
TabSize = TabSize,
116+
};
117+
}
109118
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/AggregateCompletionItemResolver.cs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5-
using System.Buffers;
65
using System.Collections.Generic;
7-
using System.Linq;
6+
using System.Collections.Immutable;
87
using System.Threading;
98
using System.Threading.Tasks;
109
using Microsoft.AspNetCore.Razor.PooledObjects;
@@ -13,26 +12,20 @@
1312

1413
namespace Microsoft.CodeAnalysis.Razor.Completion;
1514

16-
internal class AggregateCompletionItemResolver
15+
internal class AggregateCompletionItemResolver(IEnumerable<CompletionItemResolver> completionItemResolvers, ILoggerFactory loggerFactory)
1716
{
18-
private readonly IReadOnlyList<CompletionItemResolver> _completionItemResolvers;
19-
private readonly ILogger _logger;
20-
21-
public AggregateCompletionItemResolver(IEnumerable<CompletionItemResolver> completionItemResolvers, ILoggerFactory loggerFactory)
22-
{
23-
_completionItemResolvers = completionItemResolvers.ToArray();
24-
_logger = loggerFactory.GetOrCreateLogger<AggregateCompletionItemResolver>();
25-
}
17+
private readonly ImmutableArray<CompletionItemResolver> _completionItemResolvers = [.. completionItemResolvers];
18+
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<AggregateCompletionItemResolver>();
2619

2720
public async Task<VSInternalCompletionItem?> ResolveAsync(
2821
VSInternalCompletionItem item,
2922
VSInternalCompletionList containingCompletionList,
3023
ICompletionResolveContext originalRequestContext,
31-
VSInternalClientCapabilities? clientCapabilities,
24+
VSInternalClientCapabilities clientCapabilities,
3225
IComponentAvailabilityService componentAvailabilityService,
3326
CancellationToken cancellationToken)
3427
{
35-
using var completionItemResolverTasks = new PooledArrayBuilder<Task<VSInternalCompletionItem?>>(_completionItemResolvers.Count);
28+
using var completionItemResolverTasks = new PooledArrayBuilder<Task<VSInternalCompletionItem?>>(_completionItemResolvers.Length);
3629

3730
foreach (var completionItemResolver in _completionItemResolvers)
3831
{

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionItemResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal abstract class CompletionItemResolver
1313
VSInternalCompletionItem item,
1414
VSInternalCompletionList containingCompletionList,
1515
ICompletionResolveContext originalRequestContext,
16-
VSInternalClientCapabilities? clientCapabilities,
16+
VSInternalClientCapabilities clientCapabilities,
1717
IComponentAvailabilityService componentAvailabilityService,
1818
CancellationToken cancellationToken);
1919
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ public async Task<ImmutableArray<TextChange>> GetHtmlOnTypeFormattingChangesAsyn
241241
triggerCharacter: '\0',
242242
_csharpOnTypeFormattingPass,
243243
collapseChanges: true,
244-
automaticallyAddUsings: false,
244+
automaticallyAddUsings: true,
245245
validate: false,
246246
cancellationToken: cancellationToken).ConfigureAwait(false);
247247

src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.Linq;
78
using System.Threading;
89
using System.Threading.Tasks;
@@ -18,6 +19,7 @@
1819
using Microsoft.CodeAnalysis.Razor.Tooltip;
1920
using Microsoft.CodeAnalysis.Testing;
2021
using Microsoft.CodeAnalysis.Text;
22+
using Roslyn.Test.Utilities;
2123
using Xunit;
2224
using Xunit.Abstractions;
2325
using Xunit.Sdk;
@@ -214,6 +216,68 @@ async Task FooAsync()
214216
Assert.True(expectedSourceText.ContentEquals(actualSourceText));
215217
}
216218

219+
[Fact]
220+
public async Task ResolveAsync_CSharp_RemapAndFormatsTextEdit_UsingDirective()
221+
{
222+
// Arrange
223+
TestCode input =
224+
"""
225+
@{
226+
Task FooAsync()
227+
{
228+
String$$
229+
}
230+
}
231+
""";
232+
233+
// Admittedly the result here is not perfect, but only because our tests don't implement the full LSP editor logic. The key thing
234+
// is the addition of the using directive.
235+
var expectedSourceText = SourceText.From(
236+
"""
237+
@using System.Text
238+
@{
239+
Task FooAsync()
240+
{
241+
String
242+
}
243+
}
244+
""");
245+
246+
var codeDocument = CreateCodeDocument(input.Text, filePath: "C:/path/to/file.razor");
247+
// Roslyn won't send unimported types if SupportsVisualStudioExtensions is true
248+
await using var csharpServer = await CreateCSharpServerAsync(codeDocument, supportsVisualStudioExtensions: false);
249+
250+
var clientConnection = CreateClientConnectionForResolve(csharpServer);
251+
var documentContextFactory = new TestDocumentContextFactory("C:/path/to/file.razor", codeDocument);
252+
var optionsMonitor = TestRazorLSPOptionsMonitor.Create();
253+
var formattingService = await _lazyFormattingService.GetValueAsync(DisposalToken);
254+
var resolver = new DelegatedCompletionItemResolver(documentContextFactory, formattingService, DocumentMappingService, optionsMonitor, clientConnection, LoggerFactory);
255+
var containingCompletionList = await GetCompletionListAndOriginalParamsAsync(input.Position, codeDocument, csharpServer);
256+
257+
var sw = Stopwatch.StartNew();
258+
VSInternalCompletionItem item;
259+
while ((item = containingCompletionList.Items.FirstOrDefault(item => item.Label == "StringBuilder")) == null)
260+
{
261+
Assert.True(sw.Elapsed < TimeSpan.FromSeconds(5), "Failed to resolve unimported completion item after 5 second.");
262+
263+
// Roslyn only computes unimported types in the background, and we have no access to its internal workings to wait for it to be
264+
// finished, so we just have to delay and ask for completion items again.
265+
await Task.Delay(100, DisposalToken);
266+
containingCompletionList = await GetCompletionListAndOriginalParamsAsync(input.Position, codeDocument, csharpServer);
267+
}
268+
269+
Assert.NotNull(item);
270+
271+
var originalRequestContext = new DelegatedCompletionResolutionContext(_csharpCompletionParams.Identifier, _csharpCompletionParams.ProjectedKind, containingCompletionList.Data);
272+
var resolvedItem = await resolver.ResolveAsync(
273+
item, containingCompletionList, originalRequestContext, s_clientCapabilities, _componentAvailabilityService, DisposalToken);
274+
275+
var originalSourceText = SourceText.From(input.Text);
276+
var textChange = originalSourceText.GetTextChange(resolvedItem.AdditionalTextEdits.Single());
277+
var actualSourceText = originalSourceText.WithChanges(textChange);
278+
AssertEx.EqualOrDiff(expectedSourceText.ToString(), actualSourceText.ToString());
279+
}
280+
217281
[Fact]
218282
public async Task ResolveAsync_Html_Resolves()
219283
{
@@ -246,15 +310,14 @@ private async Task<VSInternalCompletionItem> ResolveCompletionItemAsync(string c
246310
{
247311
TestFileMarkupParser.GetPosition(content, out var documentContent, out var cursorPosition);
248312
var codeDocument = CreateCodeDocument(documentContent, filePath: "C:/path/to/file.razor");
249-
await using var csharpServer = await CreateCSharpServerAsync(codeDocument);
313+
await using var csharpServer = await CreateCSharpServerAsync(codeDocument, supportsVisualStudioExtensions: true);
250314

251315
var clientConnection = CreateClientConnectionForResolve(csharpServer);
252316
var documentContextFactory = new TestDocumentContextFactory("C:/path/to/file.razor", codeDocument);
253317
var optionsMonitor = TestRazorLSPOptionsMonitor.Create();
254318
var formattingService = await _lazyFormattingService.GetValueAsync(DisposalToken);
255319
var resolver = new DelegatedCompletionItemResolver(documentContextFactory, formattingService, DocumentMappingService, optionsMonitor, clientConnection, LoggerFactory);
256-
var (containingCompletionList, csharpCompletionParams) = await GetCompletionListAndOriginalParamsAsync(
257-
cursorPosition, codeDocument, csharpServer);
320+
var containingCompletionList = await GetCompletionListAndOriginalParamsAsync(cursorPosition, codeDocument, csharpServer);
258321

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

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

349+
var capabilitiesUpdater = (VSInternalClientCapabilities c) =>
350+
{
351+
c.SupportsVisualStudioExtensions = supportsVisualStudioExtensions;
352+
};
353+
286354
// Don't declare this with an 'await using'. The caller owns the lifetime of this C# LSP server.
287355
var csharpServer = await CSharpTestLspServerHelpers.CreateCSharpLspServerAsync(
288-
csharpSourceText, csharpDocumentUri, serverCapabilities, DisposalToken);
356+
csharpSourceText, csharpDocumentUri, serverCapabilities, capabilitiesUpdater, DisposalToken);
289357

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

292360
return csharpServer;
293361
}
294362

295-
private async Task<(RazorVSInternalCompletionList, DelegatedCompletionParams)> GetCompletionListAndOriginalParamsAsync(
363+
private async Task<RazorVSInternalCompletionList> GetCompletionListAndOriginalParamsAsync(
296364
int cursorPosition,
297365
RazorCodeDocument codeDocument,
298366
CSharpTestLspServer csharpServer)
299367
{
300368
var completionContext = new VSInternalCompletionContext() { TriggerKind = CompletionTriggerKind.Invoked };
301369
var documentContext = TestDocumentContext.Create("C:/path/to/file.razor", codeDocument);
302370

303-
DelegatedCompletionParams? delegatedParams = null;
304-
var clientConnection = CreateClientConnectionForCompletion(csharpServer, processParams: @params =>
305-
{
306-
delegatedParams = @params;
307-
});
371+
var clientConnection = CreateClientConnectionForCompletion(csharpServer);
308372

309373
var provider = CreateDelegatedCompletionListProvider(clientConnection);
310374

311-
var completionList = await provider.GetCompletionListAsync(
375+
return await provider.GetCompletionListAsync(
312376
codeDocument,
313377
cursorPosition,
314378
completionContext,
@@ -317,7 +381,5 @@ private async Task<CSharpTestLspServer> CreateCSharpServerAsync(RazorCodeDocumen
317381
s_defaultRazorCompletionOptions,
318382
correlationId: Guid.Empty,
319383
cancellationToken: DisposalToken);
320-
321-
return (completionList, delegatedParams);
322384
}
323385
}

src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionResolveEndpointTest.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Threading;
88
using System.Threading.Tasks;
99
using Microsoft.AspNetCore.Razor.LanguageServer.Hover;
10+
using Microsoft.AspNetCore.Razor.LanguageServer.Test;
1011
using Microsoft.AspNetCore.Razor.Test.Common;
1112
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
1213
using Microsoft.CodeAnalysis.Razor.Completion;
@@ -30,12 +31,6 @@ public RazorCompletionResolveEndpointTest(ITestOutputHelper testOutput)
3031
var projectManager = CreateProjectSnapshotManager();
3132
var componentAvailabilityService = new ComponentAvailabilityService(projectManager);
3233

33-
_endpoint = new RazorCompletionResolveEndpoint(
34-
new AggregateCompletionItemResolver(
35-
[new TestCompletionItemResolver()],
36-
LoggerFactory),
37-
_completionListCache,
38-
componentAvailabilityService);
3934
_clientCapabilities = new VSInternalClientCapabilities()
4035
{
4136
TextDocument = new TextDocumentClientCapabilities()
@@ -49,7 +44,15 @@ [new TestCompletionItemResolver()],
4944
}
5045
}
5146
};
52-
_endpoint.ApplyCapabilities(new(), _clientCapabilities);
47+
var clientCapabilitiesService = new TestClientCapabilitiesService(_clientCapabilities);
48+
49+
_endpoint = new RazorCompletionResolveEndpoint(
50+
new AggregateCompletionItemResolver(
51+
[new TestCompletionItemResolver()],
52+
LoggerFactory),
53+
_completionListCache,
54+
componentAvailabilityService,
55+
clientCapabilitiesService);
5356
}
5457

5558
[Fact]

src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ public static Task<CSharpTestLspServer> CreateCSharpLspServerAsync(
3232
CancellationToken cancellationToken) =>
3333
CreateCSharpLspServerAsync(csharpSourceText, csharpDocumentUri, serverCapabilities, new EmptyMappingService(), capabilitiesUpdater: null, cancellationToken);
3434

35+
public static Task<CSharpTestLspServer> CreateCSharpLspServerAsync(
36+
SourceText csharpSourceText,
37+
Uri csharpDocumentUri,
38+
VSInternalServerCapabilities serverCapabilities,
39+
Action<VSInternalClientCapabilities> capabilitiesUpdater,
40+
CancellationToken cancellationToken) =>
41+
CreateCSharpLspServerAsync(csharpSourceText, csharpDocumentUri, serverCapabilities, new EmptyMappingService(), capabilitiesUpdater, cancellationToken);
42+
3543
public static Task<CSharpTestLspServer> CreateCSharpLspServerAsync(
3644
SourceText csharpSourceText,
3745
Uri csharpDocumentUri,

0 commit comments

Comments
 (0)