Skip to content

Commit 27d4d4c

Browse files
authored
Use snippet InsertText in directive attributes to insert equals and quotes (#12010)
* Use sinppet InsertText in directive attributes This will insure that equals and quotes are added on attribute commit, and cursor is moved to the correct position between double quotes. * CR feedback * More CR suggestions * CR feeback - don't create another string unnecessarily * User range operator instead of Slice per analyzer suggestion * CR feedback - make local function static
1 parent e2ec62a commit 27d4d4c

File tree

5 files changed

+148
-39
lines changed

5 files changed

+148
-39
lines changed

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

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@
66
using System.Collections.Immutable;
77
using System.Linq;
88
using Microsoft.AspNetCore.Razor.Language;
9+
using Microsoft.AspNetCore.Razor.Language.Syntax;
910
using Microsoft.AspNetCore.Razor.PooledObjects;
1011
using Microsoft.CodeAnalysis.Razor.Tooltip;
1112
using Microsoft.VisualStudio.Editor.Razor;
13+
using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
1214

1315
namespace Microsoft.CodeAnalysis.Razor.Completion;
1416

1517
internal class DirectiveAttributeCompletionItemProvider : DirectiveAttributeCompletionItemProviderBase
1618
{
19+
private static ReadOnlyMemory<char> QuotedAttributeValueSnippet => "=\"$0\"".AsMemory();
20+
private static ReadOnlyMemory<char> UnquotedAttributeValueSnippet => "=$0".AsMemory();
21+
1722
public override ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorCompletionContext context)
1823
{
1924
if (!context.SyntaxTree.Options.FileKind.IsComponent())
@@ -48,7 +53,7 @@ public override ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorComp
4853

4954
// At this point we've determined that completions have been requested for the name portion of the selected attribute.
5055

51-
var completionItems = GetAttributeCompletions(attributeName, containingTagName, attributes, context.TagHelperDocumentContext);
56+
var completionItems = GetAttributeCompletions(owner, attributeName, containingTagName, attributes, context.TagHelperDocumentContext, context.Options);
5257

5358
// We don't provide Directive Attribute completions when we're in the middle of
5459
// another unrelated (doesn't start with @) partially completed attribute.
@@ -63,10 +68,12 @@ public override ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorComp
6368

6469
// Internal for testing
6570
internal static ImmutableArray<RazorCompletionItem> GetAttributeCompletions(
71+
RazorSyntaxNode containingAttribute,
6672
string selectedAttributeName,
6773
string containingTagName,
6874
ImmutableArray<string> attributes,
69-
TagHelperDocumentContext tagHelperDocumentContext)
75+
TagHelperDocumentContext tagHelperDocumentContext,
76+
RazorCompletionOptions razorCompletionOptions)
7077
{
7178
var descriptorsForTag = TagHelperFacts.GetTagHelpersGivenTag(tagHelperDocumentContext, containingTagName, parentTag: null);
7279
if (descriptorsForTag.Length == 0)
@@ -115,21 +122,35 @@ internal static ImmutableArray<RazorCompletionItem> GetAttributeCompletions(
115122

116123
foreach (var (displayText, (attributeDescriptions, commitCharacters)) in attributeCompletions)
117124
{
118-
var insertText = displayText;
125+
var insertTextSpan = displayText.AsSpan();
126+
var originalInsertTextSpan = insertTextSpan;
119127

120128
// Strip off the @ from the insertion text. This change is here to align the insertion text with the
121129
// completion hooks into VS and VSCode. Basically, completion triggers when `@` is typed so we don't
122130
// want to insert `@bind` because `@` already exists.
123-
var startIndex = insertText.StartsWith('@') ? 1 : 0;
131+
if (SpanExtensions.StartsWith(insertTextSpan, '@'))
132+
{
133+
insertTextSpan = insertTextSpan[1..];
134+
}
124135

136+
var isSnippet = false;
125137
// Indexer attribute, we don't want to insert with the triple dot.
126-
var endIndex = insertText.EndsWith("...", StringComparison.Ordinal) ? ^3 : ^0;
127-
128-
// Don't allocate a new string unless we need to make a change.
129-
if (startIndex > 0 || endIndex.Value > 0)
138+
if (MemoryExtensions.EndsWith(insertTextSpan, "...".AsSpan()))
130139
{
131-
insertText = insertText[startIndex..endIndex];
140+
insertTextSpan = insertTextSpan[..^3];
132141
}
142+
else
143+
{
144+
// We are trying for snippet text only for non-indexer attributes, e.g. *not* something like "@bind-..."
145+
if (TryGetSnippetText(containingAttribute, insertTextSpan, razorCompletionOptions, out var snippetTextSpan))
146+
{
147+
insertTextSpan = snippetTextSpan;
148+
isSnippet = true;
149+
}
150+
}
151+
152+
// Don't create another string annecessarily, even thouth ReadOnlySpan.ToString() special-cases the string to avoid allocation
153+
var insertText = insertTextSpan == originalInsertTextSpan ? displayText : insertTextSpan.ToString();
133154

134155
using var razorCommitCharacters = new PooledArrayBuilder<RazorCommitCharacter>(capacity: commitCharacters.Count);
135156

@@ -142,13 +163,40 @@ internal static ImmutableArray<RazorCompletionItem> GetAttributeCompletions(
142163
displayText,
143164
insertText,
144165
descriptionInfo: new([.. attributeDescriptions]),
145-
commitCharacters: razorCommitCharacters.ToImmutableAndClear());
166+
commitCharacters: razorCommitCharacters.ToImmutableAndClear(),
167+
isSnippet);
146168

147169
completionItems.Add(razorCompletionItem);
148170
}
149171

150172
return completionItems.ToImmutableAndClear();
151173

174+
static bool TryGetSnippetText(
175+
RazorSyntaxNode owner,
176+
ReadOnlySpan<char> baseTextSpan,
177+
RazorCompletionOptions razorCompletionOptions,
178+
out ReadOnlySpan<char> snippetTextSpan)
179+
{
180+
if (razorCompletionOptions.SnippetsSupported
181+
// Don't create snippet text when attribute is already in the tag and we are trying to replace it
182+
// Otherwise you could have something like @onabort=""=""
183+
&& owner is not (MarkupTagHelperDirectiveAttributeSyntax or MarkupAttributeBlockSyntax)
184+
&& owner.Parent is not (MarkupTagHelperDirectiveAttributeSyntax or MarkupAttributeBlockSyntax))
185+
{
186+
var suffixTextSpan = razorCompletionOptions.AutoInsertAttributeQuotes ? QuotedAttributeValueSnippet : UnquotedAttributeValueSnippet;
187+
188+
var buffer = new char[baseTextSpan.Length + suffixTextSpan.Length];
189+
baseTextSpan.CopyTo(buffer);
190+
suffixTextSpan.CopyTo(buffer.AsMemory()[baseTextSpan.Length..]);
191+
192+
snippetTextSpan = buffer.AsSpan();
193+
return true;
194+
}
195+
196+
snippetTextSpan = [];
197+
return false;
198+
}
199+
152200
bool TryAddCompletion(string attributeName, BoundAttributeDescriptor boundAttributeDescriptor, TagHelperDescriptor tagHelperDescriptor)
153201
{
154202
if (selectedAttributeName != attributeName &&

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ public static RazorCompletionItem CreateDirective(
6464
public static RazorCompletionItem CreateDirectiveAttribute(
6565
string displayText, string insertText,
6666
AggregateBoundAttributeDescription descriptionInfo,
67-
ImmutableArray<RazorCommitCharacter> commitCharacters)
68-
=> new(RazorCompletionItemKind.DirectiveAttribute, displayText, insertText, sortText: null, descriptionInfo, commitCharacters, isSnippet: false);
67+
ImmutableArray<RazorCommitCharacter> commitCharacters,
68+
bool isSnippet)
69+
=> new(RazorCompletionItemKind.DirectiveAttribute, displayText, insertText, sortText: null, descriptionInfo, commitCharacters, isSnippet);
6970

7071
public static RazorCompletionItem CreateDirectiveAttributeParameter(
7172
string displayText, string insertText,

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ public async Task ResolveAsync_DirectiveAttributeCompletion_ReturnsCompletionIte
123123
displayText: "TestItem",
124124
insertText: "TestItem",
125125
_attributeDescription,
126-
commitCharacters: []);
126+
commitCharacters: [],
127+
isSnippet: false);
127128
var completionList = CreateLSPCompletionList(razorCompletionItem);
128129
var completionItem = (VSInternalCompletionItem)completionList.Items.Single();
129130

@@ -212,7 +213,8 @@ public async Task ResolveAsync_VS_DirectiveAttributeCompletion_ReturnsCompletion
212213
var razorCompletionItem = RazorCompletionItem.CreateDirectiveAttribute(
213214
displayText: "TestItem",
214215
insertText: "TestItem", _attributeDescription,
215-
commitCharacters: []);
216+
commitCharacters: [],
217+
isSnippet: false);
216218
var completionList = CreateLSPCompletionList(razorCompletionItem);
217219
var completionItem = (VSInternalCompletionItem)completionList.Items.Single();
218220

0 commit comments

Comments
 (0)