Skip to content

Commit a00d8fe

Browse files
Bind directive attribute event parameter HTML event completions (#11804)
* Provide basic HTML form control events completions for `event` directive attribute parameter * expose event completion provider in cohosting layer, use TestCode in tests, code style * Optimize syntax tree node identification * Cohost tests * Fixes after merge Co-authored-by: David Wengier <[email protected]>
1 parent ed209e5 commit a00d8fe

File tree

9 files changed

+402
-3
lines changed

9 files changed

+402
-3
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
@@ -89,6 +89,7 @@ public static void AddCompletionServices(this IServiceCollection services)
8989
services.AddSingleton<IRazorCompletionItemProvider, DirectiveCompletionItemProvider>();
9090
services.AddSingleton<IRazorCompletionItemProvider, DirectiveAttributeCompletionItemProvider>();
9191
services.AddSingleton<IRazorCompletionItemProvider, DirectiveAttributeParameterCompletionItemProvider>();
92+
services.AddSingleton<IRazorCompletionItemProvider, DirectiveAttributeEventParameterCompletionItemProvider>();
9293
services.AddSingleton<IRazorCompletionItemProvider, DirectiveAttributeTransitionCompletionItemProvider>();
9394
services.AddSingleton<IRazorCompletionItemProvider, MarkupTransitionCompletionItemProvider>();
9495
services.AddSingleton<IRazorCompletionItemProvider, TagHelperCompletionProvider>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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.Collections.Immutable;
6+
using Microsoft.AspNetCore.Razor.Language.Syntax;
7+
using Microsoft.VisualStudio.Editor.Razor;
8+
9+
namespace Microsoft.CodeAnalysis.Razor.Completion;
10+
11+
internal class DirectiveAttributeEventParameterCompletionItemProvider : IRazorCompletionItemProvider
12+
{
13+
private static readonly ImmutableArray<RazorCompletionItem> s_eventCompletionItems = HtmlFacts.FormEvents
14+
.SelectAsArray(e => RazorCompletionItem.CreateDirectiveAttributeEventParameterHtmlEventValue(e, e, s_commitCharacters));
15+
16+
private static readonly ImmutableArray<RazorCommitCharacter> s_commitCharacters = RazorCommitCharacter.CreateArray(["\"", " ", "'"]);
17+
18+
public ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorCompletionContext context)
19+
{
20+
var owner = context.Owner?.Parent;
21+
22+
if (owner is MarkupTagHelperAttributeValueSyntax parentValueSyntax)
23+
{
24+
owner = parentValueSyntax.Parent;
25+
}
26+
27+
if (owner is not MarkupTagHelperDirectiveAttributeSyntax directiveAttributeSyntax)
28+
{
29+
return [];
30+
}
31+
32+
if (directiveAttributeSyntax is not
33+
{
34+
Colon.IsMissing: false,
35+
ParameterName: { IsMissing: false, LiteralTokens: [{ Content: "event" }] },
36+
EqualsToken.IsMissing: false,
37+
ValuePrefix.IsMissing: false,
38+
Value: { IsMissing: false } valueSyntax,
39+
})
40+
{
41+
return [];
42+
}
43+
44+
if (!valueSyntax.Span.Contains(context.AbsoluteIndex) && valueSyntax.EndPosition != context.AbsoluteIndex)
45+
{
46+
return [];
47+
}
48+
49+
return s_eventCompletionItems;
50+
}
51+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,9 @@ public static RazorCompletionItem CreateTagHelperAttribute(
8989
AggregateBoundAttributeDescription descriptionInfo,
9090
ImmutableArray<RazorCommitCharacter> commitCharacters, bool isSnippet)
9191
=> new(RazorCompletionItemKind.TagHelperAttribute, displayText, insertText, sortText, descriptionInfo, commitCharacters, isSnippet);
92+
93+
public static RazorCompletionItem CreateDirectiveAttributeEventParameterHtmlEventValue(
94+
string displayText, string insertText,
95+
ImmutableArray<RazorCommitCharacter> commitCharacters)
96+
=> new(RazorCompletionItemKind.DirectiveAttributeParameterEventValue, displayText, insertText, sortText: null, descriptionInfo: AggregateBoundAttributeDescription.Empty, commitCharacters, isSnippet: false);
9297
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ internal enum RazorCompletionItemKind
88
Directive,
99
DirectiveAttribute,
1010
DirectiveAttributeParameter,
11+
DirectiveAttributeParameterEventValue,
1112
MarkupTransition,
1213
TagHelperElement,
1314
TagHelperAttribute,

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,23 @@ internal static bool TryConvert(
189189
completionItem = parameterCompletionItem;
190190
return true;
191191
}
192+
case RazorCompletionItemKind.DirectiveAttributeParameterEventValue:
193+
{
194+
var eventValueCompletionItem = new VSInternalCompletionItem()
195+
{
196+
Label = razorCompletionItem.DisplayText,
197+
InsertText = razorCompletionItem.InsertText,
198+
FilterText = razorCompletionItem.InsertText,
199+
SortText = razorCompletionItem.SortText,
200+
InsertTextFormat = insertTextFormat,
201+
Kind = CompletionItemKind.Event,
202+
};
203+
204+
eventValueCompletionItem.UseCommitCharactersFrom(razorCompletionItem, clientCapabilities);
205+
206+
completionItem = eventValueCompletionItem;
207+
return true;
208+
}
192209
case RazorCompletionItemKind.MarkupTransition:
193210
{
194211
var markupTransitionCompletionItem = new VSInternalCompletionItem()

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/HtmlFacts.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5-
using System.Collections.Generic;
5+
using System.Collections.Frozen;
6+
using System.Collections.Immutable;
67
using Microsoft.AspNetCore.Razor.Language.Syntax;
78
using Microsoft.CodeAnalysis.Text;
89

910
namespace Microsoft.VisualStudio.Editor.Razor;
1011

1112
internal static class HtmlFacts
1213
{
13-
private static readonly HashSet<string> s_htmlSchemaTagNames = new(StringComparer.OrdinalIgnoreCase)
14+
private static readonly FrozenSet<string> s_htmlSchemaTagNames = new string[]
1415
{
1516
"DOCTYPE",
1617
"a",
@@ -135,7 +136,33 @@ internal static class HtmlFacts
135136
"var",
136137
"video",
137138
"wbr",
138-
};
139+
}.ToFrozenSet(StringComparer.Ordinal);
140+
141+
internal static readonly ImmutableArray<string> FormEvents =
142+
[
143+
"onabort",
144+
"onblur",
145+
"onchange",
146+
"onclick",
147+
"oncontextmenu",
148+
"ondblclick",
149+
"onerror",
150+
"onfocus",
151+
"oninput",
152+
"onkeydown",
153+
"onkeypress",
154+
"onkeyup",
155+
"onload",
156+
"onmousedown",
157+
"onmousemove",
158+
"onmouseout",
159+
"onmouseover",
160+
"onmouseup",
161+
"onreset",
162+
"onscroll",
163+
"onselect",
164+
"onsubmit",
165+
];
139166

140167
public static bool IsHtmlTagName(string name)
141168
=> s_htmlSchemaTagNames.Contains(name);

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPRazorCompletionItemProviders.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ internal sealed class OOPDirectiveAttributeCompletionItemProvider : DirectiveAtt
1616
[Export(typeof(IRazorCompletionItemProvider)), Shared]
1717
internal sealed class OOPDirectiveAttributeParameterCompletionItemProvider : DirectiveAttributeParameterCompletionItemProvider;
1818

19+
[Export(typeof(IRazorCompletionItemProvider)), Shared]
20+
internal sealed class OOPDirectiveAttributeEventParameterCompletionItemProvider : DirectiveAttributeEventParameterCompletionItemProvider;
21+
1922
[Export(typeof(IRazorCompletionItemProvider)), Shared]
2023
[method: ImportingConstructor]
2124
internal sealed class OOPDirectiveAttributeTransitionCompletionItemProvider(LanguageServerFeatureOptions languageServerFeatureOptions)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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 Microsoft.AspNetCore.Razor.Language;
6+
using Microsoft.AspNetCore.Razor.Language.IntegrationTests;
7+
using Microsoft.AspNetCore.Razor.Language.Syntax;
8+
using Microsoft.AspNetCore.Razor.Test.Common;
9+
using Xunit;
10+
using Xunit.Abstractions;
11+
12+
namespace Microsoft.CodeAnalysis.Razor.Completion;
13+
14+
public class DirectiveAttributeEventParameterCompletionItemProviderTest : RazorToolingIntegrationTestBase
15+
{
16+
private readonly DirectiveAttributeEventParameterCompletionItemProvider _provider;
17+
18+
internal override RazorFileKind? FileKind => RazorFileKind.Component;
19+
internal override bool UseTwoPhaseCompilation => true;
20+
21+
public DirectiveAttributeEventParameterCompletionItemProviderTest(ITestOutputHelper testOutput)
22+
: base(testOutput)
23+
{
24+
// Most of these completions rely on stuff in the web namespace.
25+
ImportItems.Add(CreateProjectItem(
26+
"_Imports.razor",
27+
"@using Microsoft.AspNetCore.Components.Web"));
28+
29+
_provider = new DirectiveAttributeEventParameterCompletionItemProvider();
30+
}
31+
32+
private RazorCodeDocument GetCodeDocument(string content)
33+
{
34+
var result = CompileToCSharp(content, throwOnFailure: false);
35+
return result.CodeDocument;
36+
}
37+
38+
[Fact]
39+
public void GetCompletionItems_OnEmptyDirectiveAttributeEventParameter_ReturnsCompletions()
40+
{
41+
// Arrange
42+
var context = CreateRazorCompletionContext("""
43+
<input @bind="str" @bind:event="$$" />
44+
45+
@code {
46+
private string? str;
47+
}
48+
""");
49+
50+
// Act
51+
var completions = _provider.GetCompletionItems(context);
52+
53+
// Assert
54+
AssertContains(completions, "oninput");
55+
AssertContains(completions, "onchange");
56+
AssertContains(completions, "onblur");
57+
}
58+
59+
[Fact]
60+
public void GetCompletionItems_OnNonEmptyDirectiveAttributeEventParameter_ReturnsCompletions()
61+
{
62+
// Arrange
63+
var context = CreateRazorCompletionContext("""
64+
<input @bind="str" @bind:event="onin$$put" />
65+
66+
@code {
67+
private string? str;
68+
}
69+
""");
70+
71+
// Act
72+
var completions = _provider.GetCompletionItems(context);
73+
74+
// Assert
75+
AssertContains(completions, "oninput");
76+
AssertContains(completions, "onchange");
77+
AssertContains(completions, "onblur");
78+
}
79+
80+
[Fact]
81+
public void GetCompletionItems_OnEmptyButNotClosedDirectiveAttributeEventParameter_ReturnsCompletions()
82+
{
83+
// Arrange
84+
var context = CreateRazorCompletionContext("""
85+
<input @bind="str" @bind:event="$$ />
86+
87+
@code {
88+
private string? str;
89+
}
90+
""");
91+
92+
// Act
93+
var completions = _provider.GetCompletionItems(context);
94+
95+
// Assert
96+
AssertContains(completions, "oninput");
97+
AssertContains(completions, "onchange");
98+
AssertContains(completions, "onblur");
99+
}
100+
101+
[Fact]
102+
public void GetCompletionItems_OnNonEmptyAndNotClosedDirectiveAttributeEventParameter_ReturnsCompletions()
103+
{
104+
// Arrange
105+
var context = CreateRazorCompletionContext("""
106+
<input @bind="str" @bind:event="oninput$$ />
107+
108+
@code {
109+
private string? str;
110+
}
111+
""");
112+
113+
// Act
114+
var completions = _provider.GetCompletionItems(context);
115+
116+
// Assert
117+
AssertContains(completions, "oninput");
118+
AssertContains(completions, "onchange");
119+
AssertContains(completions, "onblur");
120+
}
121+
122+
[Fact]
123+
public void GetCompletionItems_BeforeDirectiveAttributeEventParameterAttributePrefix_DoesNotReturnCompletions()
124+
{
125+
// Arrange
126+
var context = CreateRazorCompletionContext("""
127+
<input @bind="str" @bind:event=$$"oninput" />
128+
129+
@code {
130+
private string? str;
131+
}
132+
""");
133+
134+
// Act
135+
var completions = _provider.GetCompletionItems(context);
136+
137+
// Assert
138+
Assert.Empty(completions);
139+
}
140+
141+
[Fact]
142+
public void GetCompletionItems_AfterDirectiveAttributeEventParameterAttributeSuffix_DoesNotReturnCompletions()
143+
{
144+
// Arrange
145+
var context = CreateRazorCompletionContext("""
146+
<input @bind="str" @bind:event="oninput"$$ />
147+
148+
@code {
149+
private string? str;
150+
}
151+
""");
152+
153+
// Act
154+
var completions = _provider.GetCompletionItems(context);
155+
156+
// Assert
157+
Assert.Empty(completions);
158+
}
159+
160+
[Fact]
161+
public void GetCompletionItems_OnHtmlEventAttribute_DoesNotReturnCompletions()
162+
{
163+
// Arrange
164+
var context = CreateRazorCompletionContext("""
165+
<input @bind="str" event="oni$$nput" />
166+
167+
@code {
168+
private string? str;
169+
}
170+
""");
171+
172+
// Act
173+
var completions = _provider.GetCompletionItems(context);
174+
175+
// Assert
176+
Assert.Empty(completions);
177+
}
178+
179+
[Fact]
180+
public void GetCompletionItems_OnBindDirectiveAttribute_DoesNotReturnCompletions()
181+
{
182+
// Arrange
183+
var context = CreateRazorCompletionContext("""
184+
<input @bind="s$$tr" />
185+
186+
@code {
187+
private string? str;
188+
}
189+
""");
190+
191+
// Act
192+
var completions = _provider.GetCompletionItems(context);
193+
194+
// Assert
195+
Assert.Empty(completions);
196+
}
197+
198+
private static void AssertContains(IReadOnlyList<RazorCompletionItem> completions, string insertText)
199+
{
200+
Assert.Contains(completions, completion => insertText == completion.InsertText &&
201+
insertText == completion.DisplayText &&
202+
RazorCompletionItemKind.DirectiveAttributeParameterEventValue == completion.Kind);
203+
}
204+
205+
private RazorCompletionContext CreateRazorCompletionContext(TestCode documentContent)
206+
{
207+
var codeDocument = GetCodeDocument(documentContent.Text);
208+
var syntaxTree = codeDocument.GetSyntaxTree();
209+
var tagHelperDocumentContext = codeDocument.GetRequiredTagHelperContext();
210+
var absoluteIndex = documentContent.Position;
211+
212+
var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex);
213+
owner = AbstractRazorCompletionFactsService.AdjustSyntaxNodeForWordBoundary(owner, absoluteIndex);
214+
return new RazorCompletionContext(absoluteIndex, owner, syntaxTree, tagHelperDocumentContext);
215+
}
216+
}

0 commit comments

Comments
 (0)