6
6
using System . Collections . Immutable ;
7
7
using System . Linq ;
8
8
using Microsoft . AspNetCore . Razor . Language ;
9
+ using Microsoft . AspNetCore . Razor . Language . Syntax ;
9
10
using Microsoft . AspNetCore . Razor . PooledObjects ;
10
11
using Microsoft . CodeAnalysis . Razor . Tooltip ;
11
12
using Microsoft . VisualStudio . Editor . Razor ;
13
+ using RazorSyntaxNode = Microsoft . AspNetCore . Razor . Language . Syntax . SyntaxNode ;
12
14
13
15
namespace Microsoft . CodeAnalysis . Razor . Completion ;
14
16
15
17
internal class DirectiveAttributeCompletionItemProvider : DirectiveAttributeCompletionItemProviderBase
16
18
{
19
+ private static ReadOnlyMemory < char > QuotedAttributeValueSnippet => "=\" $0\" " . AsMemory ( ) ;
20
+ private static ReadOnlyMemory < char > UnquotedAttributeValueSnippet => "=$0" . AsMemory ( ) ;
21
+
17
22
public override ImmutableArray < RazorCompletionItem > GetCompletionItems ( RazorCompletionContext context )
18
23
{
19
24
if ( ! context . SyntaxTree . Options . FileKind . IsComponent ( ) )
@@ -48,7 +53,7 @@ public override ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorComp
48
53
49
54
// At this point we've determined that completions have been requested for the name portion of the selected attribute.
50
55
51
- var completionItems = GetAttributeCompletions ( attributeName , containingTagName , attributes , context . TagHelperDocumentContext ) ;
56
+ var completionItems = GetAttributeCompletions ( owner , attributeName , containingTagName , attributes , context . TagHelperDocumentContext , context . Options ) ;
52
57
53
58
// We don't provide Directive Attribute completions when we're in the middle of
54
59
// another unrelated (doesn't start with @) partially completed attribute.
@@ -63,10 +68,12 @@ public override ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorComp
63
68
64
69
// Internal for testing
65
70
internal static ImmutableArray < RazorCompletionItem > GetAttributeCompletions (
71
+ RazorSyntaxNode containingAttribute ,
66
72
string selectedAttributeName ,
67
73
string containingTagName ,
68
74
ImmutableArray < string > attributes ,
69
- TagHelperDocumentContext tagHelperDocumentContext )
75
+ TagHelperDocumentContext tagHelperDocumentContext ,
76
+ RazorCompletionOptions razorCompletionOptions )
70
77
{
71
78
var descriptorsForTag = TagHelperFacts . GetTagHelpersGivenTag ( tagHelperDocumentContext , containingTagName , parentTag : null ) ;
72
79
if ( descriptorsForTag . Length == 0 )
@@ -115,21 +122,35 @@ internal static ImmutableArray<RazorCompletionItem> GetAttributeCompletions(
115
122
116
123
foreach ( var ( displayText , ( attributeDescriptions , commitCharacters ) ) in attributeCompletions )
117
124
{
118
- var insertText = displayText ;
125
+ var insertTextSpan = displayText . AsSpan ( ) ;
126
+ var originalInsertTextSpan = insertTextSpan ;
119
127
120
128
// Strip off the @ from the insertion text. This change is here to align the insertion text with the
121
129
// completion hooks into VS and VSCode. Basically, completion triggers when `@` is typed so we don't
122
130
// 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
+ }
124
135
136
+ var isSnippet = false ;
125
137
// 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 ( ) ) )
130
139
{
131
- insertText = insertText [ startIndex .. endIndex ] ;
140
+ insertTextSpan = insertTextSpan [ .. ^ 3 ] ;
132
141
}
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 ( ) ;
133
154
134
155
using var razorCommitCharacters = new PooledArrayBuilder < RazorCommitCharacter > ( capacity : commitCharacters . Count ) ;
135
156
@@ -142,13 +163,40 @@ internal static ImmutableArray<RazorCompletionItem> GetAttributeCompletions(
142
163
displayText ,
143
164
insertText ,
144
165
descriptionInfo : new ( [ .. attributeDescriptions ] ) ,
145
- commitCharacters : razorCommitCharacters . ToImmutableAndClear ( ) ) ;
166
+ commitCharacters : razorCommitCharacters . ToImmutableAndClear ( ) ,
167
+ isSnippet ) ;
146
168
147
169
completionItems . Add ( razorCompletionItem ) ;
148
170
}
149
171
150
172
return completionItems . ToImmutableAndClear ( ) ;
151
173
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
+
152
200
bool TryAddCompletion ( string attributeName , BoundAttributeDescriptor boundAttributeDescriptor , TagHelperDescriptor tagHelperDescriptor )
153
201
{
154
202
if ( selectedAttributeName != attributeName &&
0 commit comments