@@ -19,6 +19,11 @@ internal class DirectiveAttributeCompletionItemProvider : DirectiveAttributeComp
1919 private static ReadOnlyMemory < char > QuotedAttributeValueSnippet => "=\" $0\" " . AsMemory ( ) ;
2020 private static ReadOnlyMemory < char > UnquotedAttributeValueSnippet => "=$0" . AsMemory ( ) ;
2121
22+ private static readonly ImmutableArray < RazorCommitCharacter > EqualsCommitCharacters = [ new ( "=" ) ] ;
23+ private static readonly ImmutableArray < RazorCommitCharacter > EqualsAndColonCommitCharacters = [ new ( "=" ) , new ( ":" ) ] ;
24+ private static readonly ImmutableArray < RazorCommitCharacter > SnippetEqualsCommitCharacters = [ new ( "=" , Insert : false ) ] ;
25+ private static readonly ImmutableArray < RazorCommitCharacter > SnippetEqualsAndColonCommitCharacters = [ new ( "=" , Insert : false ) , new ( ":" ) ] ;
26+
2227 public override ImmutableArray < RazorCompletionItem > GetCompletionItems ( RazorCompletionContext context )
2328 {
2429 if ( ! context . SyntaxTree . Options . FileKind . IsComponent ( ) )
@@ -83,7 +88,8 @@ internal static ImmutableArray<RazorCompletionItem> GetAttributeCompletions(
8388 }
8489
8590 // Use ordinal dictionary because attributes are case sensitive when matching
86- using var _ = StringDictionaryPool < ( HashSet < BoundAttributeDescriptionInfo > , HashSet < string > ) > . Ordinal . GetPooledObject ( out var attributeCompletions ) ;
91+ using var _ = StringDictionaryPool < ( ImmutableArray < BoundAttributeDescriptionInfo > , ImmutableArray < RazorCommitCharacter > ) > . Ordinal . GetPooledObject ( out var attributeCompletions ) ;
92+ var inSnippetContext = InSnippetContext ( containingAttribute , razorCompletionOptions ) ;
8793
8894 foreach ( var descriptor in descriptorsForTag )
8995 {
@@ -95,7 +101,7 @@ internal static ImmutableArray<RazorCompletionItem> GetAttributeCompletions(
95101 continue ;
96102 }
97103
98- if ( ! TryAddCompletion ( attributeDescriptor . Name , attributeDescriptor , descriptor , razorCompletionOptions ) && attributeDescriptor . Parameters . Length > 0 )
104+ if ( ! TryAddCompletion ( attributeDescriptor . Name , attributeDescriptor , descriptor , razorCompletionOptions , selectedAttributeName , attributes , inSnippetContext , attributeCompletions ) && attributeDescriptor . Parameters . Length > 0 )
99105 {
100106 // This attribute has parameters and the base attribute name (@bind) is already satisfied. We need to check if there are any valid
101107 // parameters left to be provided, if so, we need to still represent the base attribute name in the completion list.
@@ -105,15 +111,15 @@ internal static ImmutableArray<RazorCompletionItem> GetAttributeCompletions(
105111 if ( ! attributes . Any ( name => TagHelperMatchingConventions . SatisfiesBoundAttributeWithParameter ( parameterDescriptor , name , attributeDescriptor ) ) )
106112 {
107113 // This bound attribute parameter has not had a completion entry added for it, re-represent the base attribute name in the completion list
108- AddCompletion ( attributeDescriptor . Name , attributeDescriptor , descriptor , razorCompletionOptions ) ;
114+ AddCompletion ( attributeDescriptor . Name , attributeDescriptor , descriptor , razorCompletionOptions , inSnippetContext , attributeCompletions ) ;
109115 break ;
110116 }
111117 }
112118 }
113119
114120 if ( ! attributeDescriptor . IndexerNamePrefix . IsNullOrEmpty ( ) )
115121 {
116- TryAddCompletion ( attributeDescriptor . IndexerNamePrefix + "..." , attributeDescriptor , descriptor , razorCompletionOptions ) ;
122+ TryAddCompletion ( attributeDescriptor . IndexerNamePrefix + "..." , attributeDescriptor , descriptor , razorCompletionOptions , selectedAttributeName , attributes , inSnippetContext , attributeCompletions ) ;
117123 }
118124 }
119125 }
@@ -142,62 +148,62 @@ internal static ImmutableArray<RazorCompletionItem> GetAttributeCompletions(
142148 else
143149 {
144150 // 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 ) )
151+ if ( inSnippetContext )
146152 {
147- insertTextSpan = snippetTextSpan ;
153+ GetSnippetText ( insertTextSpan , razorCompletionOptions , out insertTextSpan ) ;
148154 isSnippet = true ;
149155 }
150156 }
151157
152- // Don't create another string annecessarily , even thouth ReadOnlySpan.ToString() special-cases the string to avoid allocation
158+ // Don't create another string unnecessarily , even though ReadOnlySpan.ToString() special-cases the string to avoid allocation
153159 var insertText = insertTextSpan == originalInsertTextSpan ? displayText : insertTextSpan . ToString ( ) ;
154160
155- using var razorCommitCharacters = new PooledArrayBuilder < RazorCommitCharacter > ( capacity : commitCharacters . Count ) ;
156-
157- foreach ( var c in commitCharacters )
158- {
159- razorCommitCharacters . Add ( new ( c ) ) ;
160- }
161-
162161 var razorCompletionItem = RazorCompletionItem . CreateDirectiveAttribute (
163162 displayText ,
164163 insertText ,
165- descriptionInfo : new ( [ .. attributeDescriptions ] ) ,
166- commitCharacters : razorCommitCharacters . ToImmutableAndClear ( ) ,
164+ descriptionInfo : new ( attributeDescriptions ) ,
165+ commitCharacters ,
167166 isSnippet ) ;
168167
169168 completionItems . Add ( razorCompletionItem ) ;
170169 }
171170
172171 return completionItems . ToImmutableAndClear ( ) ;
173172
174- static bool TryGetSnippetText (
173+ static bool InSnippetContext (
175174 RazorSyntaxNode owner ,
176- ReadOnlySpan < char > baseTextSpan ,
177- RazorCompletionOptions razorCompletionOptions ,
178- out ReadOnlySpan < char > snippetTextSpan )
175+ RazorCompletionOptions razorCompletionOptions )
179176 {
180- if ( razorCompletionOptions . SnippetsSupported
177+ return razorCompletionOptions . SnippetsSupported
181178 // Don't create snippet text when attribute is already in the tag and we are trying to replace it
182179 // Otherwise you could have something like @onabort=""=""
183180 && owner is not ( MarkupTagHelperDirectiveAttributeSyntax or MarkupAttributeBlockSyntax )
184- && owner . Parent is not ( MarkupTagHelperDirectiveAttributeSyntax or MarkupAttributeBlockSyntax ) )
185- {
186- var suffixTextSpan = razorCompletionOptions . AutoInsertAttributeQuotes ? QuotedAttributeValueSnippet : UnquotedAttributeValueSnippet ;
181+ && owner . Parent is not ( MarkupTagHelperDirectiveAttributeSyntax or MarkupAttributeBlockSyntax ) ;
182+ }
187183
188- var buffer = new char [ baseTextSpan . Length + suffixTextSpan . Length ] ;
189- baseTextSpan . CopyTo ( buffer ) ;
190- suffixTextSpan . CopyTo ( buffer . AsMemory ( ) [ baseTextSpan . Length ..] ) ;
184+ static void GetSnippetText (
185+ ReadOnlySpan < char > baseTextSpan ,
186+ RazorCompletionOptions razorCompletionOptions ,
187+ out ReadOnlySpan < char > snippetTextSpan )
188+ {
189+ var suffixTextSpan = razorCompletionOptions . AutoInsertAttributeQuotes ? QuotedAttributeValueSnippet : UnquotedAttributeValueSnippet ;
191190
192- snippetTextSpan = buffer . AsSpan ( ) ;
193- return true ;
194- }
191+ var buffer = new char [ baseTextSpan . Length + suffixTextSpan . Length ] ;
192+ baseTextSpan . CopyTo ( buffer ) ;
193+ suffixTextSpan . CopyTo ( buffer . AsMemory ( ) [ baseTextSpan . Length .. ] ) ;
195194
196- snippetTextSpan = [ ] ;
197- return false ;
195+ snippetTextSpan = buffer . AsSpan ( ) ;
198196 }
199197
200- bool TryAddCompletion ( string attributeName , BoundAttributeDescriptor boundAttributeDescriptor , TagHelperDescriptor tagHelperDescriptor , RazorCompletionOptions razorCompletionOptions )
198+ static bool TryAddCompletion (
199+ string attributeName ,
200+ BoundAttributeDescriptor boundAttributeDescriptor ,
201+ TagHelperDescriptor tagHelperDescriptor ,
202+ RazorCompletionOptions razorCompletionOptions ,
203+ string selectedAttributeName ,
204+ ImmutableArray < string > attributes ,
205+ bool inSnippetContext ,
206+ Dictionary < string , ( ImmutableArray < BoundAttributeDescriptionInfo > , ImmutableArray < RazorCommitCharacter > ) > attributeCompletions )
201207 {
202208 if ( selectedAttributeName != attributeName &&
203209 attributes . Any ( attributeName , static ( name , attributeName ) => name == attributeName ) )
@@ -207,60 +213,85 @@ bool TryAddCompletion(string attributeName, BoundAttributeDescriptor boundAttrib
207213 return false ;
208214 }
209215
210- AddCompletion ( attributeName , boundAttributeDescriptor , tagHelperDescriptor , razorCompletionOptions ) ;
216+ AddCompletion ( attributeName , boundAttributeDescriptor , tagHelperDescriptor , razorCompletionOptions , inSnippetContext , attributeCompletions ) ;
211217 return true ;
212218 }
213219
214- void AddCompletion ( string attributeName , BoundAttributeDescriptor boundAttributeDescriptor , TagHelperDescriptor tagHelperDescriptor , RazorCompletionOptions razorCompletionOptions )
220+ static void AddCompletion (
221+ string attributeName ,
222+ BoundAttributeDescriptor boundAttributeDescriptor ,
223+ TagHelperDescriptor tagHelperDescriptor ,
224+ RazorCompletionOptions razorCompletionOptions ,
225+ bool inSnippetContext ,
226+ Dictionary < string , ( ImmutableArray < BoundAttributeDescriptionInfo > , ImmutableArray < RazorCommitCharacter > ) > attributeCompletions )
215227 {
216228 if ( ! attributeCompletions . TryGetValue ( attributeName , out var attributeDetails ) )
217229 {
218230 attributeDetails = ( [ ] , [ ] ) ;
219- attributeCompletions [ attributeName ] = attributeDetails ;
220231 }
221232
222233 ( var attributeDescriptions , var commitCharacters ) = attributeDetails ;
223234
224235 var indexerCompletion = attributeName . EndsWith ( "..." , StringComparison . Ordinal ) ;
225236 var tagHelperTypeName = tagHelperDescriptor . TypeName ;
226237 var descriptionInfo = BoundAttributeDescriptionInfo . From ( boundAttributeDescriptor , isIndexer : indexerCompletion , tagHelperTypeName ) ;
227- attributeDescriptions . Add ( descriptionInfo ) ;
228238
229- if ( indexerCompletion )
239+ if ( ! attributeDescriptions . Contains ( descriptionInfo ) )
230240 {
231- // Indexer attribute, we don't want to commit with standard chars
232- return ;
241+ attributeDescriptions = attributeDescriptions . Add ( descriptionInfo ) ;
233242 }
234243
235- if ( ! razorCompletionOptions . UseVsCodeCompletionCommitCharacters )
244+ // Verify not an indexer attribute, as those don't commit with standard chars
245+ if ( ! indexerCompletion )
236246 {
237- // We don't add "=" as a commit character when using VSCode trigger characters.
238- commitCharacters . Add ( "= ") ;
239- }
247+ var equalsAdded = commitCharacters . Any ( static c => c . Character == "=" ) ;
248+ var spaceAdded = commitCharacters . Any ( static c => c . Character == " ") ;
249+ var colonAdded = commitCharacters . Any ( static c => c . Character == ":" ) ;
240250
241- var spaceAdded = commitCharacters . Contains ( " " ) ;
242- var colonAdded = commitCharacters . Contains ( ":" ) ;
251+ // We don't add "=" as a commit character when using VSCode trigger characters.
252+ equalsAdded |= ! razorCompletionOptions . UseVsCodeCompletionCommitCharacters ;
243253
244- if ( ! spaceAdded || ! colonAdded )
245- {
246254 foreach ( var boundAttribute in tagHelperDescriptor . BoundAttributes )
247255 {
248- if ( ! spaceAdded && boundAttribute . IsBooleanProperty )
256+ spaceAdded |= boundAttribute . IsBooleanProperty ;
257+ colonAdded |= boundAttribute . Parameters . Length > 0 ;
258+
259+ if ( spaceAdded && colonAdded )
260+ {
261+ break ;
262+ }
263+ }
264+
265+ // Determine if we have a common commit character set
266+ commitCharacters = ( equalsAdded , spaceAdded , colonAdded , inSnippetContext ) switch
267+ {
268+ ( true , false , false , false ) => EqualsCommitCharacters ,
269+ ( true , false , true , false ) => EqualsAndColonCommitCharacters ,
270+ ( true , false , false , true ) => SnippetEqualsCommitCharacters ,
271+ ( true , false , true , true ) => SnippetEqualsAndColonCommitCharacters ,
272+ _ => [ ]
273+ } ;
274+
275+ if ( commitCharacters . IsEmpty )
276+ {
277+ if ( equalsAdded )
249278 {
250- commitCharacters . Add ( " " ) ;
251- spaceAdded = true ;
279+ commitCharacters = commitCharacters . Add ( new ( "=" , Insert : ! inSnippetContext ) ) ;
252280 }
253- else if ( ! colonAdded && boundAttribute . Parameters . Length > 0 )
281+
282+ if ( spaceAdded )
254283 {
255- commitCharacters . Add ( ":" ) ;
256- colonAdded = true ;
284+ commitCharacters = commitCharacters . Add ( new ( " " ) ) ;
257285 }
258- else if ( spaceAdded && colonAdded )
286+
287+ if ( colonAdded )
259288 {
260- break ;
289+ commitCharacters = commitCharacters . Add ( new ( ":" ) ) ;
261290 }
262291 }
263292 }
293+
294+ attributeCompletions [ attributeName ] = ( attributeDescriptions , commitCharacters ) ;
264295 }
265296 }
266297}
0 commit comments