Skip to content

Commit c1e400a

Browse files
authored
Add the RazorCSharp keywords to completion (#12522)
Add the csharp razor keywords to show up in completion after typing an '@' Should fix #6927 and part of #12483 From David's analysis, which I didn't stray too far away from: When Roslyn introduced semantic snippets, it broke Razor because it wouldn't offer the if snippet outside of a code block. This is because until you actually write code after an @ in Razor, the compiler doesn't know if it should go in the class, or the render method. Roslyn is "smart" enough not to show if as a completion option if you're at the class level, and of course as soon as you do type @if the Razor compiler is smart enough to put that in the render method, and everyone is happy. Except the user who was just trying to type :) So as a consequence, we turned off semantic snippets for Razor files, as a temporary workaround. 3 years later is still temporary, right? Anyway, that "turn off for Razor files" is probably broken for cohosting, so we're back in this position. So the plan, in #6927, is for us to add back our own snippets for @ if (and maybe @ for, @ foreach etc.) to this list and then I think we might be good.
1 parent d4a50d9 commit c1e400a

File tree

26 files changed

+356
-28
lines changed

26 files changed

+356
-28
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public static void AddCompletionServices(this IServiceCollection services)
8787
services.AddSingleton<CompletionItemResolver, DelegatedCompletionItemResolver>();
8888
services.AddSingleton<ITagHelperCompletionService, TagHelperCompletionService>();
8989
services.AddSingleton<IRazorCompletionFactsService, LspRazorCompletionFactsService>();
90+
services.AddSingleton<IRazorCompletionItemProvider, CSharpRazorKeywordCompletionItemProvider>();
9091
services.AddSingleton<IRazorCompletionItemProvider, DirectiveCompletionItemProvider>();
9192
services.AddSingleton<IRazorCompletionItemProvider, DirectiveAttributeCompletionItemProvider>();
9293
services.AddSingleton<IRazorCompletionItemProvider, DirectiveAttributeEventParameterCompletionItemProvider>();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.CodeAnalysis.Razor.Completion;
5+
6+
internal sealed class CSharpRazorKeywordCompletionDescription(string description) : CompletionDescription
7+
{
8+
public override string Description { get; } = string.Format(SR.KeywordDescription, description);
9+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Collections.Immutable;
6+
using System.Runtime.InteropServices;
7+
using Microsoft.AspNetCore.Razor.Language;
8+
using Microsoft.AspNetCore.Razor.Language.Syntax;
9+
10+
namespace Microsoft.CodeAnalysis.Razor.Completion;
11+
12+
internal class CSharpRazorKeywordCompletionItemProvider : IRazorCompletionItemProvider
13+
{
14+
internal static readonly ImmutableArray<RazorCommitCharacter> KeywordCommitCharacters = RazorCommitCharacter.CreateArray([" "]);
15+
16+
// internal for testing
17+
internal static readonly ImmutableArray<string> CSharpRazorKeywords =
18+
[
19+
"do", "for", "foreach", "if", "lock", "switch", "try", "while"
20+
];
21+
22+
// Internal for testing
23+
internal static readonly ImmutableArray<RazorCompletionItem> CSharpRazorKeywordCompletionItems = GetCSharpRazorKeywordCompletionItems();
24+
25+
public ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorCompletionContext context)
26+
{
27+
return ShouldProvideCompletions(context)
28+
? CSharpRazorKeywordCompletionItems
29+
: [];
30+
}
31+
32+
// Internal for testing
33+
internal static bool ShouldProvideCompletions(RazorCompletionContext context)
34+
{
35+
var owner = context.Owner;
36+
if (owner is null)
37+
{
38+
return false;
39+
}
40+
41+
// Do not provide IntelliSense for explicit expressions. Explicit expressions will usually look like:
42+
// @(DateTime.Now)
43+
var implicitExpression = owner.FirstAncestorOrSelf<CSharpImplicitExpressionSyntax>();
44+
if (implicitExpression is null)
45+
{
46+
return false;
47+
}
48+
49+
if (implicitExpression.Width > 2 && context.Reason != CompletionReason.Invoked)
50+
{
51+
// We only want to provide razor csharp keyword completions if the implicit expression is empty "@|" or at the beginning of a word "@i|", this ensures
52+
// we're consistent with how C# typically provides completion items.
53+
return false;
54+
}
55+
56+
if (owner.ChildNodesAndTokens().Any(static x => !x.AsToken(out var token) || !IsCSharpRazorKeywordCompletableToken(token)))
57+
{
58+
// Implicit expression contains nodes or tokens that aren't completable by a csharp razor keyword
59+
return false;
60+
}
61+
62+
return true;
63+
}
64+
65+
private static bool IsCSharpRazorKeywordCompletableToken(AspNetCore.Razor.Language.Syntax.SyntaxToken token)
66+
{
67+
return token is { Kind: SyntaxKind.Identifier or SyntaxKind.Marker or SyntaxKind.Keyword }
68+
or { Kind: SyntaxKind.Transition, Parent.Kind: SyntaxKind.CSharpTransition };
69+
}
70+
71+
private static ImmutableArray<RazorCompletionItem> GetCSharpRazorKeywordCompletionItems()
72+
{
73+
var completionItems = new RazorCompletionItem[CSharpRazorKeywords.Length];
74+
75+
for (var i = 0; i < CSharpRazorKeywords.Length; i++)
76+
{
77+
var keyword = CSharpRazorKeywords[i];
78+
79+
var keywordCompletionItem = RazorCompletionItem.CreateKeyword(
80+
displayText: keyword,
81+
insertText: keyword,
82+
KeywordCommitCharacters);
83+
84+
completionItems[i] = keywordCompletionItem;
85+
}
86+
87+
return ImmutableCollectionsMarshal.AsImmutableArray(completionItems);
88+
}
89+
}
Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
64
namespace Microsoft.CodeAnalysis.Razor.Completion;
75

8-
internal class DirectiveCompletionDescription : CompletionDescription
6+
internal sealed class DirectiveCompletionDescription(string description) : CompletionDescription
97
{
10-
public override string Description { get; }
11-
12-
public DirectiveCompletionDescription(string description)
13-
{
14-
if (description is null)
15-
{
16-
throw new ArgumentNullException(nameof(description));
17-
}
18-
19-
Description = description;
20-
}
8+
public override string Description { get; } = description;
219
}
Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
64
namespace Microsoft.CodeAnalysis.Razor.Completion;
75

8-
internal class MarkupTransitionCompletionDescription : CompletionDescription
6+
internal sealed class MarkupTransitionCompletionDescription(string description) : CompletionDescription
97
{
10-
public override string Description { get; }
11-
12-
public MarkupTransitionCompletionDescription(string description)
13-
{
14-
if (description is null)
15-
{
16-
throw new ArgumentNullException(nameof(description));
17-
}
18-
19-
Description = description;
20-
}
8+
public override string Description { get; } = description;
219
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,9 @@ public static RazorCompletionItem CreateAttribute(
114114
AttributeDescriptionInfo descriptionInfo,
115115
ImmutableArray<RazorCommitCharacter> commitCharacters, bool isSnippet)
116116
=> new(RazorCompletionItemKind.Attribute, displayText, insertText, sortText: null, descriptionInfo, commitCharacters, isSnippet);
117+
118+
public static RazorCompletionItem CreateKeyword(
119+
string displayText, string insertText,
120+
ImmutableArray<RazorCommitCharacter> commitCharacters)
121+
=> new(RazorCompletionItemKind.CSharpRazorKeyword, displayText, insertText, sortText: null, new CSharpRazorKeywordCompletionDescription(displayText), commitCharacters, isSnippet: false);
117122
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace Microsoft.CodeAnalysis.Razor.Completion;
55

66
internal enum RazorCompletionItemKind
77
{
8+
CSharpRazorKeyword,
89
Directive,
910
DirectiveAttribute,
1011
DirectiveAttributeParameter,

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,15 @@ internal class RazorCompletionItemResolver : CompletionItemResolver
142142
.ConfigureAwait(false);
143143
}
144144

145+
break;
146+
}
147+
case RazorCompletionItemKind.CSharpRazorKeyword:
148+
{
149+
if (associatedRazorCompletion.DescriptionInfo is CSharpRazorKeywordCompletionDescription descriptionInfo)
150+
{
151+
completionItem.Documentation = descriptionInfo.Description;
152+
}
153+
145154
break;
146155
}
147156
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,23 @@ internal static bool TryConvert(
276276
completionItem = attributeCompletionItem;
277277
return true;
278278
}
279+
case RazorCompletionItemKind.CSharpRazorKeyword:
280+
{
281+
var csharpRazorKeywordCompletionItem = new VSInternalCompletionItem()
282+
{
283+
Label = razorCompletionItem.DisplayText,
284+
InsertText = razorCompletionItem.InsertText,
285+
FilterText = razorCompletionItem.DisplayText,
286+
SortText = razorCompletionItem.SortText,
287+
InsertTextFormat = insertTextFormat,
288+
Kind = CompletionItemKind.Keyword,
289+
};
290+
291+
csharpRazorKeywordCompletionItem.UseCommitCharactersFrom(razorCompletionItem, clientCapabilities);
292+
293+
completionItem = csharpRazorKeywordCompletionItem;
294+
return true;
295+
}
279296
}
280297

281298
completionItem = null;

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,7 @@
230230
<value>{0} (and req'd attributes...)</value>
231231
<comment>The term "req'd" is an abbreviation for "required"</comment>
232232
</data>
233+
<data name="KeywordDescription" xml:space="preserve">
234+
<value>{0} Keyword</value>
235+
</data>
233236
</root>

0 commit comments

Comments
 (0)