Skip to content

Commit e0517b8

Browse files
Cleanup CompletionTriggerAndCommitCharacters (#11600)
Recently, I was taking a look at completion performance and noticed that `CompletionTriggerAndCommitCharacters` has gotten into a halfway state where it's sometimes accessed statically, and sometimes as an instance. This change refactors `CompletionTriggerAndCommitCharacters` to be fully instance-based and cleans it up significantly.
2 parents b8cab05 + e34b4fc commit e0517b8

File tree

14 files changed

+176
-120
lines changed

14 files changed

+176
-120
lines changed

src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCompletionBenchmark.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ public async Task SetupAsync()
4141
var documentMappingService = lspServices.GetRequiredService<IDocumentMappingService>();
4242
var clientConnection = lspServices.GetRequiredService<IClientConnection>();
4343
var completionListCache = lspServices.GetRequiredService<CompletionListCache>();
44-
var completionTriggerAndCommitCharacters = lspServices.GetRequiredService<CompletionTriggerAndCommitCharacters>();
44+
var triggerAndCommitCharacters = lspServices.GetRequiredService<CompletionTriggerAndCommitCharacters>();
4545
var loggerFactory = lspServices.GetRequiredService<ILoggerFactory>();
4646

47-
var delegatedCompletionListProvider = new TestDelegatedCompletionListProvider(documentMappingService, clientConnection, completionListCache, completionTriggerAndCommitCharacters);
48-
var completionListProvider = new CompletionListProvider(razorCompletionListProvider, delegatedCompletionListProvider);
47+
var delegatedCompletionListProvider = new TestDelegatedCompletionListProvider(documentMappingService, clientConnection, completionListCache, triggerAndCommitCharacters);
48+
var completionListProvider = new CompletionListProvider(razorCompletionListProvider, delegatedCompletionListProvider, triggerAndCommitCharacters);
4949
var configurationService = new DefaultRazorConfigurationService(clientConnection, loggerFactory);
5050
var optionsMonitor = new RazorLSPOptionsMonitor(configurationService, RazorLSPOptions.Default);
51-
CompletionEndpoint = new RazorCompletionEndpoint(completionListProvider, completionTriggerAndCommitCharacters, telemetryReporter: null, optionsMonitor);
51+
CompletionEndpoint = new RazorCompletionEndpoint(completionListProvider, triggerAndCommitCharacters, telemetryReporter: null, optionsMonitor);
5252

5353
var clientCapabilities = new VSInternalClientCapabilities
5454
{

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,14 @@
1313

1414
namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion;
1515

16-
internal class CompletionListProvider
16+
internal class CompletionListProvider(
17+
RazorCompletionListProvider razorCompletionListProvider,
18+
DelegatedCompletionListProvider delegatedCompletionListProvider,
19+
CompletionTriggerAndCommitCharacters triggerAndCommitCharacters)
1720
{
18-
private readonly RazorCompletionListProvider _razorCompletionListProvider;
19-
private readonly DelegatedCompletionListProvider _delegatedCompletionListProvider;
20-
21-
public CompletionListProvider(RazorCompletionListProvider razorCompletionListProvider, DelegatedCompletionListProvider delegatedCompletionListProvider)
22-
{
23-
_razorCompletionListProvider = razorCompletionListProvider;
24-
_delegatedCompletionListProvider = delegatedCompletionListProvider;
25-
}
21+
private readonly RazorCompletionListProvider _razorCompletionListProvider = razorCompletionListProvider;
22+
private readonly DelegatedCompletionListProvider _delegatedCompletionListProvider = delegatedCompletionListProvider;
23+
private readonly CompletionTriggerAndCommitCharacters _triggerAndCommitCharacters = triggerAndCommitCharacters;
2624

2725
public async Task<VSInternalCompletionList?> GetCompletionListAsync(
2826
int absoluteIndex,
@@ -34,7 +32,7 @@ public CompletionListProvider(RazorCompletionListProvider razorCompletionListPro
3432
CancellationToken cancellationToken)
3533
{
3634
// First we delegate to get completion items from the individual language server
37-
var delegatedCompletionList = CompletionTriggerAndCommitCharacters.IsValidTrigger(_delegatedCompletionListProvider.TriggerCharacters, completionContext)
35+
var delegatedCompletionList = _triggerAndCommitCharacters.IsValidDelegationTrigger(completionContext)
3836
? await _delegatedCompletionListProvider.GetCompletionListAsync(
3937
absoluteIndex,
4038
completionContext,
@@ -51,7 +49,7 @@ public CompletionListProvider(RazorCompletionListProvider razorCompletionListPro
5149
: null;
5250

5351
// Now we get the Razor completion list, using information from the actual language server if necessary
54-
var razorCompletionList = CompletionTriggerAndCommitCharacters.IsValidTrigger(_razorCompletionListProvider.TriggerCharacters, completionContext)
52+
var razorCompletionList = _triggerAndCommitCharacters.IsValidRazorTrigger(completionContext)
5553
? await _razorCompletionListProvider.GetCompletionListAsync(
5654
absoluteIndex,
5755
completionContext,

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal class DelegatedCompletionListProvider
2727
private readonly IDocumentMappingService _documentMappingService;
2828
private readonly IClientConnection _clientConnection;
2929
private readonly CompletionListCache _completionListCache;
30-
private readonly CompletionTriggerAndCommitCharacters _completionTriggerAndCommitCharacters;
30+
private readonly CompletionTriggerAndCommitCharacters _triggerAndCommitCharacters;
3131

3232
public DelegatedCompletionListProvider(
3333
IDocumentMappingService documentMappingService,
@@ -38,12 +38,9 @@ public DelegatedCompletionListProvider(
3838
_documentMappingService = documentMappingService;
3939
_clientConnection = clientConnection;
4040
_completionListCache = completionListCache;
41-
_completionTriggerAndCommitCharacters = completionTriggerAndCommitCharacters;
41+
_triggerAndCommitCharacters = completionTriggerAndCommitCharacters;
4242
}
4343

44-
// virtual for tests
45-
public virtual FrozenSet<string> TriggerCharacters => _completionTriggerAndCommitCharacters.AllDelegationTriggerCharacters;
46-
4744
// virtual for tests
4845
public virtual async Task<VSInternalCompletionList?> GetCompletionListAsync(
4946
int absoluteIndex,
@@ -77,7 +74,7 @@ public DelegatedCompletionListProvider(
7774
positionInfo = provisionalCompletionValue.DocumentPositionInfo;
7875
}
7976

80-
if (DelegatedCompletionHelper.RewriteContext(completionContext, positionInfo.LanguageKind, _completionTriggerAndCommitCharacters) is not { } rewrittenContext)
77+
if (DelegatedCompletionHelper.RewriteContext(completionContext, positionInfo.LanguageKind, _triggerAndCommitCharacters) is not { } rewrittenContext)
8178
{
8279
return null;
8380
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal class RazorCompletionEndpoint(
2525
: IRazorRequestHandler<CompletionParams, VSInternalCompletionList?>, ICapabilitiesProvider
2626
{
2727
private readonly CompletionListProvider _completionListProvider = completionListProvider;
28-
private readonly CompletionTriggerAndCommitCharacters _completionTriggerAndCommitCharacters = completionTriggerAndCommitCharacters;
28+
private readonly CompletionTriggerAndCommitCharacters _triggerAndCommitCharacters = completionTriggerAndCommitCharacters;
2929
private readonly ITelemetryReporter? _telemetryReporter = telemetryReporter;
3030
private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor;
3131

@@ -40,8 +40,8 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V
4040
serverCapabilities.CompletionProvider = new CompletionOptions()
4141
{
4242
ResolveProvider = true,
43-
TriggerCharacters = _completionTriggerAndCommitCharacters.AllTriggerCharacters,
44-
AllCommitCharacters = CompletionTriggerAndCommitCharacters.AllCommitCharacters
43+
TriggerCharacters = [.. _triggerAndCommitCharacters.AllTriggerCharacters],
44+
AllCommitCharacters = [.. _triggerAndCommitCharacters.AllCommitCharacters]
4545
};
4646
}
4747

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.Collections.Immutable;
6+
using Microsoft.CodeAnalysis.Razor.Workspaces;
7+
using Microsoft.VisualStudio.LanguageServer.Protocol;
8+
9+
namespace Microsoft.CodeAnalysis.Razor.Completion;
10+
11+
internal class CompletionTriggerAndCommitCharacters
12+
{
13+
/// <summary>
14+
/// Trigger character that can trigger both Razor and Delegation completion
15+
/// </summary>
16+
private const char TransitionCharacter = '@';
17+
18+
private static readonly char[] s_vsHtmlTriggerCharacters = [':', '#', '.', '!', '*', ',', '(', '[', '-', '<', '&', '\\', '/', '\'', '"', '=', ':', ' ', '`'];
19+
private static readonly char[] s_vsCodeHtmlTriggerCharacters = ['#', '.', '!', ',', '-', '<'];
20+
private static readonly char[] s_razorTriggerCharacters = ['<', ':', ' '];
21+
private static readonly char[] s_csharpTriggerCharacters = [' ', '(', '=', '#', '.', '<', '[', '{', '"', '/', ':', '~'];
22+
private static readonly char[] s_commitCharacters = [' ', '>', ';', '='];
23+
24+
private readonly HashSet<char> _csharpTriggerCharacters;
25+
private readonly HashSet<char> _delegationTriggerCharacters;
26+
private readonly HashSet<char> _htmlTriggerCharacters;
27+
private readonly HashSet<char> _razorTriggerCharacters;
28+
29+
public ImmutableArray<string> AllTriggerCharacters { get; }
30+
31+
/// <summary>
32+
/// This is the intersection of C# and HTML commit characters.
33+
/// </summary>
34+
// We need to specify it so that platform can correctly calculate ApplicableToSpan in
35+
// https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/RemoteLanguage/Impl/Features/Completion/AsyncCompletionSource.cs&version=GBdevelop&line=855&lineEnd=855&lineStartColumn=9&lineEndColumn=49&lineStyle=plain&_a=contents
36+
// This is needed to fix https://github.com/dotnet/razor/issues/10787 in particular
37+
public ImmutableArray<string> AllCommitCharacters { get; }
38+
39+
public CompletionTriggerAndCommitCharacters(LanguageServerFeatureOptions languageServerFeatureOptions)
40+
{
41+
// C# trigger characters (do NOT include '@')
42+
var csharpTriggerCharacters = new HashSet<char>();
43+
csharpTriggerCharacters.UnionWith(s_csharpTriggerCharacters);
44+
45+
// HTML trigger characters (include '@' + HTML trigger characters)
46+
var htmlTriggerCharacters = new HashSet<char>() { TransitionCharacter };
47+
48+
if (languageServerFeatureOptions.UseVsCodeCompletionTriggerCharacters)
49+
{
50+
htmlTriggerCharacters.UnionWith(s_vsCodeHtmlTriggerCharacters);
51+
}
52+
else
53+
{
54+
htmlTriggerCharacters.UnionWith(s_vsHtmlTriggerCharacters);
55+
}
56+
57+
// Delegation trigger characters (include '@' + C# and HTML trigger characters)
58+
var delegationTriggerCharacters = new HashSet<char> { TransitionCharacter };
59+
delegationTriggerCharacters.UnionWith(csharpTriggerCharacters);
60+
delegationTriggerCharacters.UnionWith(htmlTriggerCharacters);
61+
62+
// Razor trigger characters (include '@' + Razor trigger characters)
63+
var razorTriggerCharacters = new HashSet<char>() { TransitionCharacter };
64+
razorTriggerCharacters.UnionWith(s_razorTriggerCharacters);
65+
66+
// All trigger characters (include Razor + Delegation trigger characters)
67+
var allTriggerCharacters = new HashSet<char>();
68+
allTriggerCharacters.UnionWith(razorTriggerCharacters);
69+
allTriggerCharacters.UnionWith(delegationTriggerCharacters);
70+
71+
var commitCharacters = new HashSet<char>();
72+
commitCharacters.UnionWith(s_commitCharacters);
73+
74+
_csharpTriggerCharacters = csharpTriggerCharacters;
75+
_htmlTriggerCharacters = htmlTriggerCharacters;
76+
_razorTriggerCharacters = razorTriggerCharacters;
77+
_delegationTriggerCharacters = delegationTriggerCharacters;
78+
AllTriggerCharacters = allTriggerCharacters.SelectAsArray(static c => c.ToString());
79+
AllCommitCharacters = commitCharacters.SelectAsArray(static c => c.ToString());
80+
}
81+
82+
public bool IsValidCSharpTrigger(CompletionContext completionContext)
83+
=> IsValidTrigger(completionContext, _csharpTriggerCharacters);
84+
85+
public bool IsValidDelegationTrigger(CompletionContext completionContext)
86+
=> IsValidTrigger(completionContext, _delegationTriggerCharacters);
87+
88+
public bool IsValidHtmlTrigger(CompletionContext completionContext)
89+
=> IsValidTrigger(completionContext, _htmlTriggerCharacters);
90+
91+
public bool IsValidRazorTrigger(CompletionContext completionContext)
92+
=> IsValidTrigger(completionContext, _razorTriggerCharacters);
93+
94+
private static bool IsValidTrigger(CompletionContext completionContext, HashSet<char> triggerCharacters)
95+
=> completionContext.TriggerKind != CompletionTriggerKind.TriggerCharacter ||
96+
completionContext.TriggerCharacter is not [var c] ||
97+
triggerCharacters.Contains(c);
98+
99+
public bool IsCSharpTriggerCharacter(string ch)
100+
=> ch is [var c] && _csharpTriggerCharacters.Contains(c);
101+
102+
public bool IsDelegationTriggerCharacter(string ch)
103+
=> ch is [var c] && _delegationTriggerCharacters.Contains(c);
104+
105+
public bool IsHtmlTriggerCharacter(string ch)
106+
=> ch is [var c] && _htmlTriggerCharacters.Contains(c);
107+
108+
public bool IsRazorTriggerCharacter(string ch)
109+
=> ch is [var c] && _razorTriggerCharacters.Contains(c);
110+
111+
public bool IsTransitionCharacter(string ch)
112+
=> ch is [TransitionCharacter];
113+
}

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

Lines changed: 0 additions & 46 deletions
This file was deleted.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ internal static class DelegatedCompletionHelper
3737
/// </summary>
3838
/// <param name="context">Original completion context passed to the completion handler</param>
3939
/// <param name="languageKind">Language of the completion position</param>
40-
/// <param name="completionTriggerAndCommitCharacters">Per-client set of trigger and commit characters</param>
40+
/// <param name="triggerAndCommitCharacters">Per-client set of trigger and commit characters</param>
4141
/// <returns>Possibly modified completion context</returns>
4242
/// <remarks>For example, if we invoke C# completion in Razor via @ character, we will not
4343
/// want C# to see @ as the trigger character and instead will transform completion context
4444
/// into "invoked" and "explicit" rather than "typing", without a trigger character</remarks>
4545
public static VSInternalCompletionContext? RewriteContext(
4646
VSInternalCompletionContext context,
4747
RazorLanguageKind languageKind,
48-
CompletionTriggerAndCommitCharacters completionTriggerAndCommitCharacters)
48+
CompletionTriggerAndCommitCharacters triggerAndCommitCharacters)
4949
{
5050
Debug.Assert(languageKind != RazorLanguageKind.Razor,
5151
$"{nameof(RewriteContext)} should be called for delegated completion only");
@@ -58,7 +58,7 @@ internal static class DelegatedCompletionHelper
5858
}
5959

6060
if (languageKind == RazorLanguageKind.CSharp
61-
&& CompletionTriggerAndCommitCharacters.CSharpTriggerCharacters.Contains(triggerCharacter))
61+
&& triggerAndCommitCharacters.IsCSharpTriggerCharacter(triggerCharacter))
6262
{
6363
// C# trigger character for C# content
6464
return context;
@@ -69,7 +69,7 @@ internal static class DelegatedCompletionHelper
6969
// For HTML we don't want to delegate to HTML language server is completion is due to a trigger characters that is not
7070
// HTML trigger character. Doing so causes bad side effects in VSCode HTML client as we will end up with non-matching
7171
// completion entries
72-
return completionTriggerAndCommitCharacters.HtmlTriggerCharacters.Contains(triggerCharacter) ? context : null;
72+
return triggerAndCommitCharacters.IsHtmlTriggerCharacter(triggerCharacter) ? context : null;
7373
}
7474

7575
// Trigger character not associated with the current language. Transform the context into an invoked context.
@@ -80,7 +80,7 @@ internal static class DelegatedCompletionHelper
8080
};
8181

8282
if (languageKind == RazorLanguageKind.CSharp
83-
&& CompletionTriggerAndCommitCharacters.RazorDelegationTriggerCharacters.Contains(triggerCharacter))
83+
&& triggerAndCommitCharacters.IsTransitionCharacter(triggerCharacter))
8484
{
8585
// The C# language server will not return any completions for the '@' character unless we
8686
// send the completion request explicitly.

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Frozen;
65
using System.Collections.Generic;
76
using System.Collections.Immutable;
87
using System.Diagnostics;
@@ -32,9 +31,6 @@ internal class RazorCompletionListProvider(
3231
Title = SR.ReTrigger_Completions_Title,
3332
};
3433

35-
// virtual for tests
36-
public virtual FrozenSet<string> TriggerCharacters => CompletionTriggerAndCommitCharacters.RazorTriggerCharacters;
37-
3834
// virtual for tests
3935
public virtual async Task<VSInternalCompletionList?> GetCompletionListAsync(
4036
int absoluteIndex,

0 commit comments

Comments
 (0)