Skip to content

Commit b7b4752

Browse files
authored
Add option for element completion commit characters (#9379)
Resolves the "we did not find firm opinions" part of #8295 by introducing an option to allow users to control how they want completion for html elements, tag helper elements and components to behave. Corresponding VS Code PR: dotnet/vscode-csharp#6506
2 parents 65e84b3 + 2df13ff commit b7b4752

31 files changed

+504
-57
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Razor.LanguageServer.Protocol;
9+
using Microsoft.VisualStudio.LanguageServer.Protocol;
10+
11+
namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation;
12+
13+
internal class HtmlCommitCharacterResponseRewriter(RazorLSPOptionsMonitor razorLSPOptionsMonitor) : DelegatedCompletionResponseRewriter
14+
{
15+
private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor;
16+
17+
public override int Order => ExecutionBehaviorOrder.ChangesCompletionItems;
18+
19+
public override Task<VSInternalCompletionList> RewriteAsync(VSInternalCompletionList completionList, int hostDocumentIndex, DocumentContext hostDocumentContext, DelegatedCompletionParams delegatedParameters, CancellationToken cancellationToken)
20+
{
21+
if (delegatedParameters.ProjectedKind != RazorLanguageKind.Html)
22+
{
23+
return Task.FromResult(completionList);
24+
}
25+
26+
if (_razorLSPOptionsMonitor.CurrentValue.CommitElementsWithSpace)
27+
{
28+
return Task.FromResult(completionList);
29+
}
30+
31+
string[]? itemCommitChars = null;
32+
if (completionList.CommitCharacters is { } commitCharacters)
33+
{
34+
if (commitCharacters.TryGetFirst(out var commitChars))
35+
{
36+
itemCommitChars = commitChars.Where(s => s != " ").ToArray();
37+
38+
// If the default commit characters didn't include " " already, then we set our list to null to avoid over-specifying commit characters
39+
if (itemCommitChars.Length == commitChars.Length)
40+
{
41+
itemCommitChars = null;
42+
}
43+
}
44+
else if (commitCharacters.TryGetSecond(out var vsCommitChars))
45+
{
46+
itemCommitChars = vsCommitChars.Where(s => s.Character != " ").Select(s => s.Character).ToArray();
47+
48+
// If the default commit characters didn't include " " already, then we set our list to null to avoid over-specifying commit characters
49+
if (itemCommitChars.Length == vsCommitChars.Length)
50+
{
51+
itemCommitChars = null;
52+
}
53+
}
54+
}
55+
56+
foreach (var item in completionList.Items)
57+
{
58+
if (item.Kind == CompletionItemKind.Element)
59+
{
60+
if (item.CommitCharacters is null)
61+
{
62+
if (itemCommitChars is not null)
63+
{
64+
// This item wants to use the default commit characters, so change it to our updated version of them, without the space
65+
item.CommitCharacters = itemCommitChars;
66+
}
67+
}
68+
else
69+
{
70+
// This item has its own commit characters, so just remove spaces
71+
item.CommitCharacters = item.CommitCharacters?.Where(s => s != " ").ToArray();
72+
}
73+
}
74+
}
75+
76+
return Task.FromResult(completionList);
77+
}
78+
}

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal class TagHelperCompletionProvider : IRazorCompletionItemProvider
2525
internal static readonly IReadOnlyList<RazorCommitCharacter> AttributeSnippetCommitCharacters = RazorCommitCharacter.FromArray(new[] { "=" }, insert: false);
2626

2727
private static readonly IReadOnlyList<RazorCommitCharacter> s_elementCommitCharacters = RazorCommitCharacter.FromArray(new[] { " ", ">" });
28+
private static readonly IReadOnlyList<RazorCommitCharacter> s_elementCommitCharacters_WithoutSpace = RazorCommitCharacter.FromArray(new[] { ">" });
2829
private static readonly IReadOnlyList<RazorCommitCharacter> s_noCommitCharacters = Array.Empty<RazorCommitCharacter>();
2930
private readonly HtmlFactsService _htmlFactsService;
3031
private readonly TagHelperCompletionService _tagHelperCompletionService;
@@ -110,7 +111,7 @@ public ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorCompletionCon
110111
var stringifiedAttributes = _tagHelperFactsService.StringifyAttributes(attributes);
111112

112113
return GetAttributeCompletions(parent, containingTagNameToken.Content, selectedAttributeName, stringifiedAttributes, context.TagHelperDocumentContext, context.Options);
113-
114+
114115
static bool InOrAtEndOfAttribute(SyntaxNode attributeSyntax, int absoluteIndex)
115116
{
116117
// When we are in the middle of writing an attribute it is treated as a minimilized one, e.g.:
@@ -248,13 +249,17 @@ private ImmutableArray<RazorCompletionItem> GetElementCompletions(
248249
var completionResult = _tagHelperCompletionService.GetElementCompletions(elementCompletionContext);
249250
using var completionItems = new PooledArrayBuilder<RazorCompletionItem>();
250251

252+
var commitChars = _optionsMonitor.CurrentValue.CommitElementsWithSpace
253+
? s_elementCommitCharacters
254+
: s_elementCommitCharacters_WithoutSpace;
255+
251256
foreach (var (displayText, tagHelpers) in completionResult.Completions)
252257
{
253258
var razorCompletionItem = new RazorCompletionItem(
254259
displayText: displayText,
255260
insertText: displayText,
256261
kind: RazorCompletionItemKind.TagHelperElement,
257-
commitCharacters: s_elementCommitCharacters);
262+
commitCharacters: commitChars);
258263

259264
var tagHelperDescriptions = tagHelpers.SelectAsArray(BoundElementDescriptionInfo.From);
260265
var elementDescription = new AggregateBoundElementDescription(tagHelperDescriptions);

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultRazorConfigurationService.cs

+16-3
Original file line numberDiff line numberDiff line change
@@ -94,23 +94,27 @@ internal RazorLSPOptions BuildOptions(JObject[] result)
9494
}
9595
else
9696
{
97-
ExtractVSCodeOptions(result, out var trace, out var enableFormatting, out var autoClosingTags);
98-
return new RazorLSPOptions(trace, enableFormatting, autoClosingTags, ClientSettings.Default);
97+
ExtractVSCodeOptions(result, out var trace, out var enableFormatting, out var autoClosingTags, out var commitElementsWithSpace);
98+
return new RazorLSPOptions(trace, enableFormatting, autoClosingTags, commitElementsWithSpace, ClientSettings.Default);
9999
}
100100
}
101101

102102
private void ExtractVSCodeOptions(
103103
JObject[] result,
104104
out Trace trace,
105105
out bool enableFormatting,
106-
out bool autoClosingTags)
106+
out bool autoClosingTags,
107+
out bool commitElementsWithSpace)
107108
{
108109
var razor = result[0];
109110
var html = result[1];
110111

111112
trace = RazorLSPOptions.Default.Trace;
112113
enableFormatting = RazorLSPOptions.Default.EnableFormatting;
113114
autoClosingTags = RazorLSPOptions.Default.AutoClosingTags;
115+
// Deliberately not using the "default" here because we want a different default for VS Code, as
116+
// this matches VS Code's html servers commit behaviour
117+
commitElementsWithSpace = false;
114118

115119
if (razor != null)
116120
{
@@ -127,6 +131,15 @@ private void ExtractVSCodeOptions(
127131
enableFormatting = GetObjectOrDefault(parsedEnableFormatting, enableFormatting);
128132
}
129133
}
134+
135+
if (razor.TryGetValue("completion", out var parsedCompletion))
136+
{
137+
if (parsedCompletion is JObject jObject &&
138+
jObject.TryGetValue("commitElementsWithSpace", out var parsedCommitElementsWithSpace))
139+
{
140+
commitElementsWithSpace = GetObjectOrDefault(parsedCommitElementsWithSpace, commitElementsWithSpace);
141+
}
142+
}
130143
}
131144

132145
if (html != null)

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

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public static void AddCompletionServices(this IServiceCollection services, Langu
8989
services.AddSingleton<RazorCompletionListProvider>();
9090
services.AddSingleton<DelegatedCompletionResponseRewriter, TextEditResponseRewriter>();
9191
services.AddSingleton<DelegatedCompletionResponseRewriter, DesignTimeHelperResponseRewriter>();
92+
services.AddSingleton<DelegatedCompletionResponseRewriter, HtmlCommitCharacterResponseRewriter>();
9293

9394
services.AddSingleton<AggregateCompletionItemResolver>();
9495
services.AddSingleton<CompletionItemResolver, RazorCompletionItemResolver>();

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ internal record RazorLSPOptions(
1414
int TabSize,
1515
bool FormatOnType,
1616
bool AutoInsertAttributeQuotes,
17-
bool ColorBackground)
17+
bool ColorBackground,
18+
bool CommitElementsWithSpace)
1819
{
19-
public RazorLSPOptions(Trace trace, bool enableFormatting, bool autoClosingTags, ClientSettings settings)
20-
: this(trace, enableFormatting, autoClosingTags, !settings.ClientSpaceSettings.IndentWithTabs, settings.ClientSpaceSettings.IndentSize, settings.AdvancedSettings.FormatOnType, settings.AdvancedSettings.AutoInsertAttributeQuotes, settings.AdvancedSettings.ColorBackground)
20+
public RazorLSPOptions(Trace trace, bool enableFormatting, bool autoClosingTags, bool commitElementsWithSpace, ClientSettings settings)
21+
: this(trace, enableFormatting, autoClosingTags, !settings.ClientSpaceSettings.IndentWithTabs, settings.ClientSpaceSettings.IndentSize, settings.AdvancedSettings.FormatOnType, settings.AdvancedSettings.AutoInsertAttributeQuotes, settings.AdvancedSettings.ColorBackground, commitElementsWithSpace)
2122
{
2223
}
2324

24-
public readonly static RazorLSPOptions Default = new(Trace: default, EnableFormatting: true, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, FormatOnType: true, AutoInsertAttributeQuotes: true, ColorBackground: false);
25+
public readonly static RazorLSPOptions Default = new(Trace: default, EnableFormatting: true, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, FormatOnType: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CommitElementsWithSpace: true);
2526

2627
public LogLevel MinLogLevel => GetLogLevelForTrace(Trace);
2728

@@ -42,5 +43,6 @@ internal static RazorLSPOptions From(ClientSettings clientSettings)
4243
=> new(Default.Trace,
4344
Default.EnableFormatting,
4445
clientSettings.AdvancedSettings.AutoClosingTags,
46+
clientSettings.AdvancedSettings.CommitElementsWithSpace,
4547
clientSettings);
4648
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Editor/ClientSettings.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal sealed record ClientSpaceSettings(bool IndentWithTabs, int IndentSize)
2525
public int IndentSize { get; } = IndentSize >= 0 ? IndentSize : throw new ArgumentOutOfRangeException(nameof(IndentSize));
2626
}
2727

28-
internal sealed record ClientAdvancedSettings(bool FormatOnType, bool AutoClosingTags, bool AutoInsertAttributeQuotes, bool ColorBackground)
28+
internal sealed record ClientAdvancedSettings(bool FormatOnType, bool AutoClosingTags, bool AutoInsertAttributeQuotes, bool ColorBackground, bool CommitElementsWithSpace)
2929
{
30-
public static readonly ClientAdvancedSettings Default = new(FormatOnType: true, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: false);
30+
public static readonly ClientAdvancedSettings Default = new(FormatOnType: true, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CommitElementsWithSpace: true);
3131
}

src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Options/OptionsStorage.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ internal class OptionsStorage : IAdvancedSettingsStorage
2626
private const string AutoClosingTagsName = "AutoClosingTags";
2727
private const string AutoInsertAttributeQuotesName = "AutoInsertAttributeQuotes";
2828
private const string ColorBackgroundName = "ColorBackground";
29+
private const string CommitElementsWithSpaceName = "CommitElementsWithSpace";
2930

3031
public bool FormatOnType
3132
{
@@ -51,6 +52,12 @@ public bool ColorBackground
5152
set => SetBool(ColorBackgroundName, value);
5253
}
5354

55+
public bool CommitElementsWithSpace
56+
{
57+
get => GetBool(CommitElementsWithSpaceName, defaultValue: true);
58+
set => SetBool(CommitElementsWithSpaceName, value);
59+
}
60+
5461
[ImportingConstructor]
5562
public OptionsStorage(SVsServiceProvider vsServiceProvider, ITelemetryReporter telemetryReporter)
5663
{
@@ -63,7 +70,7 @@ public OptionsStorage(SVsServiceProvider vsServiceProvider, ITelemetryReporter t
6370

6471
public event EventHandler<ClientAdvancedSettingsChangedEventArgs>? Changed;
6572

66-
public ClientAdvancedSettings GetAdvancedSettings() => new(FormatOnType, AutoClosingTags, AutoInsertAttributeQuotes, ColorBackground);
73+
public ClientAdvancedSettings GetAdvancedSettings() => new(FormatOnType, AutoClosingTags, AutoInsertAttributeQuotes, ColorBackground, CommitElementsWithSpace);
6774

6875
public bool GetBool(string name, bool defaultValue)
6976
{

src/Razor/src/Microsoft.VisualStudio.RazorExtension/Options/AdvancedOptionPage.cs

+16
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal class AdvancedOptionPage : DialogPage
1919
private bool? _autoClosingTags;
2020
private bool? _autoInsertAttributeQuotes;
2121
private bool? _colorBackground;
22+
private bool? _commitElementsWithSpace;
2223

2324
public AdvancedOptionPage()
2425
{
@@ -58,6 +59,15 @@ public bool AutoInsertAttributeQuotes
5859
set => _autoInsertAttributeQuotes = value;
5960
}
6061

62+
[LocCategory(nameof(VSPackage.Completion))]
63+
[LocDescription(nameof(VSPackage.Setting_CommitElementsWithSpaceDescription))]
64+
[LocDisplayName(nameof(VSPackage.Setting_CommitElementsWithSpaceDisplayName))]
65+
public bool CommitElementsWithSpace
66+
{
67+
get => _commitElementsWithSpace ?? _optionsStorage.Value.CommitElementsWithSpace;
68+
set => _commitElementsWithSpace = value;
69+
}
70+
6171
[LocCategory(nameof(VSPackage.Formatting))]
6272
[LocDescription(nameof(VSPackage.Setting_ColorBackgroundDescription))]
6373
[LocDisplayName(nameof(VSPackage.Setting_ColorBackgroundDisplayName))]
@@ -88,6 +98,11 @@ protected override void OnApply(PageApplyEventArgs e)
8898
{
8999
_optionsStorage.Value.ColorBackground = _colorBackground.Value;
90100
}
101+
102+
if (_commitElementsWithSpace is not null)
103+
{
104+
_optionsStorage.Value.CommitElementsWithSpace = _commitElementsWithSpace.Value;
105+
}
91106
}
92107

93108
protected override void OnClosed(EventArgs e)
@@ -96,5 +111,6 @@ protected override void OnClosed(EventArgs e)
96111
_autoClosingTags = null;
97112
_autoInsertAttributeQuotes = null;
98113
_colorBackground = null;
114+
_commitElementsWithSpace = null;
99115
}
100116
}

src/Razor/src/Microsoft.VisualStudio.RazorExtension/VSPackage.resx

+33-27
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -165,6 +165,12 @@
165165
<data name="Setting_ColorBackgroundDisplayName" xml:space="preserve">
166166
<value>Background for C# Code</value>
167167
</data>
168+
<data name="Setting_CommitElementsWithSpaceDescription" xml:space="preserve">
169+
<value>If true, pressing space will commit suggestions for Html elements and components</value>
170+
</data>
171+
<data name="Setting_CommitElementsWithSpaceDisplayName" xml:space="preserve">
172+
<value>Commit Elements with Space</value>
173+
</data>
168174
<data name="Setting_FormattingOnTypeDescription" xml:space="preserve">
169175
<value>If true, formatting will be enabled while typing</value>
170176
</data>

src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.cs.xlf

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)