Skip to content

Commit d7d70a6

Browse files
author
N. Taylor Mullen
committed
Add support for directive attribute descriptions
- Updated the `RazorCompletionItem` type to no longer contain a `Documentation`/`Description` property because in completion scenarios descriptions are never readily available and don't make a lot of sense being on the completion item itself. This is also how Roslyn does completions. - We now utilize an item bag on `RazorCompletionItem`s to hold description data. Currently there's just two type of completion descriptions (directive and attribute). Based off of the completion item kind (directive or attribute) we can extract the appropriate description. The goal of this new description type was to represent all data for a completion in an unstructured way so each platform can display it using its own capabilities. - Added a description factory to convert description items into classified containers. Tried to replicate WTE's description look as much as possible. - Left out documentation parsing. Will do this at a later date; however directive attributes rarely have documentation specific pieces that require parsing so we handle the 90% case. - Updated existing tests to reflect the new documentation mechanisms for RazorCompletionItems. - Added new tests to validate the completion source and the description factory. dotnet/aspnetcore#6364
1 parent c84fd3a commit d7d70a6

21 files changed

+753
-88
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.CodeAnalysis.Razor.Completion
8+
{
9+
internal class AttributeCompletionDescription
10+
{
11+
public AttributeCompletionDescription(IReadOnlyList<AttributeDescriptionInfo> descriptionInfos)
12+
{
13+
if (descriptionInfos == null)
14+
{
15+
throw new ArgumentNullException(nameof(descriptionInfos));
16+
}
17+
18+
DescriptionInfos = descriptionInfos;
19+
}
20+
21+
public IReadOnlyList<AttributeDescriptionInfo> DescriptionInfos { get; }
22+
}
23+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Extensions.Internal;
6+
7+
namespace Microsoft.CodeAnalysis.Razor.Completion
8+
{
9+
internal sealed class AttributeDescriptionInfo : IEquatable<AttributeDescriptionInfo>
10+
{
11+
public AttributeDescriptionInfo(
12+
string returnTypeName,
13+
string typeName,
14+
string propertyName,
15+
string documentation)
16+
{
17+
if (returnTypeName is null)
18+
{
19+
throw new ArgumentNullException(nameof(returnTypeName));
20+
}
21+
22+
if (typeName is null)
23+
{
24+
throw new ArgumentNullException(nameof(typeName));
25+
}
26+
27+
if (propertyName is null)
28+
{
29+
throw new ArgumentNullException(nameof(propertyName));
30+
}
31+
32+
ReturnTypeName = returnTypeName;
33+
TypeName = typeName;
34+
PropertyName = propertyName;
35+
Documentation = documentation ?? string.Empty;
36+
}
37+
38+
public string ReturnTypeName { get; }
39+
40+
public string TypeName { get; }
41+
42+
public string PropertyName { get; }
43+
44+
public string Documentation { get; }
45+
46+
public bool Equals(AttributeDescriptionInfo other)
47+
{
48+
if (!string.Equals(ReturnTypeName, other.ReturnTypeName, StringComparison.Ordinal))
49+
{
50+
return false;
51+
}
52+
53+
if (!string.Equals(TypeName, other.TypeName, StringComparison.Ordinal))
54+
{
55+
return false;
56+
}
57+
58+
if (!string.Equals(PropertyName, other.PropertyName, StringComparison.Ordinal))
59+
{
60+
return false;
61+
}
62+
63+
if (!string.Equals(Documentation, other.Documentation, StringComparison.Ordinal))
64+
{
65+
return false;
66+
}
67+
68+
return true;
69+
}
70+
71+
public override int GetHashCode()
72+
{
73+
var combiner = HashCodeCombiner.Start();
74+
combiner.Add(ReturnTypeName);
75+
combiner.Add(TypeName);
76+
combiner.Add(PropertyName);
77+
combiner.Add(Documentation);
78+
79+
return combiner.CombinedHash;
80+
}
81+
}
82+
}

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ internal IReadOnlyList<RazorCompletionItem> GetAttributeCompletions(
9494
}
9595

9696
// Attributes are case sensitive when matching
97-
var attributeCompletions = new HashSet<string>(StringComparer.Ordinal);
97+
var attributeCompletions = new Dictionary<string, HashSet<AttributeDescriptionInfo>>(StringComparer.Ordinal);
9898
for (var i = 0; i < descriptorsForTag.Count; i++)
9999
{
100100
var descriptor = descriptorsForTag[i];
@@ -107,7 +107,7 @@ internal IReadOnlyList<RazorCompletionItem> GetAttributeCompletions(
107107
continue;
108108
}
109109

110-
if (!TryAddCompletion(attributeDescriptor.Name) && attributeDescriptor.BoundAttributeParameters.Count > 0)
110+
if (!TryAddCompletion(attributeDescriptor.Name, attributeDescriptor, descriptor) && attributeDescriptor.BoundAttributeParameters.Count > 0)
111111
{
112112
// This attribute has parameters and the base attribute name (@bind) is already satisfied. We need to check if there are any valid
113113
// parameters left to be provided, if so, we need to still represent the base attribute name in the completion list.
@@ -118,41 +118,42 @@ internal IReadOnlyList<RazorCompletionItem> GetAttributeCompletions(
118118
if (!attributes.Any(name => TagHelperMatchingConventions.SatisfiesBoundAttributeWithParameter(name, attributeDescriptor, parameterDescriptor)))
119119
{
120120
// This bound attribute parameter has not had a completion entry added for it, re-represent the base attribute name in the completion list
121-
attributeCompletions.Add(attributeDescriptor.Name);
121+
AddCompletion(attributeDescriptor.Name, attributeDescriptor, descriptor);
122122
break;
123123
}
124124
}
125125
}
126126

127127
if (!string.IsNullOrEmpty(attributeDescriptor.IndexerNamePrefix))
128128
{
129-
TryAddCompletion(attributeDescriptor.IndexerNamePrefix + "...");
129+
TryAddCompletion(attributeDescriptor.IndexerNamePrefix + "...", attributeDescriptor, descriptor);
130130
}
131131
}
132132
}
133133

134134
var completionItems = new List<RazorCompletionItem>();
135135
foreach (var completion in attributeCompletions)
136136
{
137-
var insertText = completion;
137+
var insertText = completion.Key;
138138
if (insertText.EndsWith("..."))
139139
{
140140
// Indexer attribute, we don't want to insert with the triple dot.
141141
insertText = insertText.Substring(0, insertText.Length - 3);
142142
}
143143

144144
var razorCompletionItem = new RazorCompletionItem(
145-
completion,
145+
completion.Key,
146146
insertText,
147-
description: string.Empty,
148147
RazorCompletionItemKind.DirectiveAttribute);
148+
var completionDescription = new AttributeCompletionDescription(completion.Value.ToArray());
149+
razorCompletionItem.SetAttributeCompletionDescription(completionDescription);
149150

150151
completionItems.Add(razorCompletionItem);
151152
}
152153

153154
return completionItems;
154155

155-
bool TryAddCompletion(string attributeName)
156+
bool TryAddCompletion(string attributeName, BoundAttributeDescriptor boundAttributeDescriptor, TagHelperDescriptor tagHelperDescriptor)
156157
{
157158
if (attributes.Any(name => string.Equals(name, attributeName, StringComparison.Ordinal)) &&
158159
!string.Equals(selectedAttributeName, attributeName, StringComparison.Ordinal))
@@ -162,10 +163,25 @@ bool TryAddCompletion(string attributeName)
162163
return false;
163164
}
164165

165-
attributeCompletions.Add(attributeName);
166-
166+
AddCompletion(attributeName, boundAttributeDescriptor, tagHelperDescriptor);
167167
return true;
168168
}
169+
170+
void AddCompletion(string attributeName, BoundAttributeDescriptor boundAttributeDescriptor, TagHelperDescriptor tagHelperDescriptor)
171+
{
172+
if (!attributeCompletions.TryGetValue(attributeName, out var attributeDescriptionInfos))
173+
{
174+
attributeDescriptionInfos = new HashSet<AttributeDescriptionInfo>();
175+
attributeCompletions[attributeName] = attributeDescriptionInfos;
176+
}
177+
178+
var descriptionInfo = new AttributeDescriptionInfo(
179+
boundAttributeDescriptor.TypeName,
180+
tagHelperDescriptor.GetTypeName(),
181+
boundAttributeDescriptor.GetPropertyName(),
182+
boundAttributeDescriptor.Documentation);
183+
attributeDescriptionInfos.Add(descriptionInfo);
184+
}
169185
}
170186
}
171187
}

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ internal IReadOnlyList<RazorCompletionItem> GetAttributeParameterCompletions(
9292
}
9393

9494
// Attribute parameters are case sensitive when matching
95-
var attributeCompletions = new HashSet<string>(StringComparer.Ordinal);
95+
var attributeCompletions = new Dictionary<string, HashSet<AttributeDescriptionInfo>>(StringComparer.Ordinal);
9696
foreach (var descriptor in descriptorsForTag)
9797
{
9898
for (var i = 0; i < descriptor.BoundAttributes.Count; i++)
@@ -116,7 +116,18 @@ internal IReadOnlyList<RazorCompletionItem> GetAttributeParameterCompletions(
116116
continue;
117117
}
118118

119-
attributeCompletions.Add(parameterDescriptor.Name);
119+
if (!attributeCompletions.TryGetValue(parameterDescriptor.Name, out var attributeDescriptionInfos))
120+
{
121+
attributeDescriptionInfos = new HashSet<AttributeDescriptionInfo>();
122+
attributeCompletions[parameterDescriptor.Name] = attributeDescriptionInfos;
123+
}
124+
125+
var descriptionInfo = new AttributeDescriptionInfo(
126+
parameterDescriptor.TypeName,
127+
descriptor.GetTypeName(),
128+
parameterDescriptor.GetPropertyName(),
129+
parameterDescriptor.Documentation);
130+
attributeDescriptionInfos.Add(descriptionInfo);
120131
}
121132
}
122133
}
@@ -125,18 +136,19 @@ internal IReadOnlyList<RazorCompletionItem> GetAttributeParameterCompletions(
125136
var completionItems = new List<RazorCompletionItem>();
126137
foreach (var completion in attributeCompletions)
127138
{
128-
if (string.Equals(completion, parameterName, StringComparison.Ordinal))
139+
if (string.Equals(completion.Key, parameterName, StringComparison.Ordinal))
129140
{
130141
// This completion is identical to the selected parameter, don't provide for completions for what's already
131142
// present in the document.
132143
continue;
133144
}
134145

135146
var razorCompletionItem = new RazorCompletionItem(
136-
completion,
137-
completion,
138-
description: string.Empty,
147+
completion.Key,
148+
completion.Key,
139149
RazorCompletionItemKind.DirectiveAttributeParameter);
150+
var completionDescription = new AttributeCompletionDescription(completion.Value.ToArray());
151+
razorCompletionItem.SetAttributeCompletionDescription(completionDescription);
140152

141153
completionItems.Add(razorCompletionItem);
142154
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.CodeAnalysis.Razor.Completion
7+
{
8+
internal class DirectiveCompletionDescription
9+
{
10+
public DirectiveCompletionDescription(string description)
11+
{
12+
if (description == null)
13+
{
14+
throw new ArgumentNullException(nameof(description));
15+
}
16+
17+
Description = description;
18+
}
19+
20+
public string Description { get; }
21+
}
22+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ internal static List<RazorCompletionItem> GetDirectiveCompletionItems(RazorSynta
108108
var completionItem = new RazorCompletionItem(
109109
completionDisplayText,
110110
directive.Directive,
111-
directive.Description,
112111
RazorCompletionItemKind.Directive);
112+
var completionDescription = new DirectiveCompletionDescription(directive.Description);
113+
completionItem.SetDirectiveCompletionDescription(completionDescription);
113114
completionItems.Add(completionItem);
114115
}
115116

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

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Linq;
6+
using Microsoft.AspNetCore.Razor.Language;
57
using Microsoft.Extensions.Internal;
68

79
namespace Microsoft.CodeAnalysis.Razor.Completion
810
{
911
internal sealed class RazorCompletionItem : IEquatable<RazorCompletionItem>
1012
{
13+
private ItemCollection _items;
14+
1115
public RazorCompletionItem(
12-
string displayText,
13-
string insertText,
14-
string description,
16+
string displayText,
17+
string insertText,
1518
RazorCompletionItemKind kind)
1619
{
1720
if (displayText == null)
@@ -24,25 +27,36 @@ public RazorCompletionItem(
2427
throw new ArgumentNullException(nameof(insertText));
2528
}
2629

27-
if (description == null)
28-
{
29-
throw new ArgumentNullException(nameof(description));
30-
}
31-
3230
DisplayText = displayText;
3331
InsertText = insertText;
34-
Description = description;
3532
Kind = kind;
3633
}
3734

3835
public string DisplayText { get; }
3936

4037
public string InsertText { get; }
4138

42-
public string Description { get; }
43-
4439
public RazorCompletionItemKind Kind { get; }
4540

41+
public ItemCollection Items
42+
{
43+
get
44+
{
45+
if (_items == null)
46+
{
47+
lock (this)
48+
{
49+
if (_items == null)
50+
{
51+
_items = new ItemCollection();
52+
}
53+
}
54+
}
55+
56+
return _items;
57+
}
58+
}
59+
4660
public override bool Equals(object obj)
4761
{
4862
return Equals(obj as RazorCompletionItem);
@@ -65,12 +79,12 @@ public bool Equals(RazorCompletionItem other)
6579
return false;
6680
}
6781

68-
if (!string.Equals(Description, other.Description, StringComparison.Ordinal))
82+
if (Kind != other.Kind)
6983
{
7084
return false;
7185
}
7286

73-
if (Kind != other.Kind)
87+
if (!Enumerable.SequenceEqual(Items, other.Items))
7488
{
7589
return false;
7690
}
@@ -83,7 +97,6 @@ public override int GetHashCode()
8397
var hashCodeCombiner = HashCodeCombiner.Start();
8498
hashCodeCombiner.Add(DisplayText);
8599
hashCodeCombiner.Add(InsertText);
86-
hashCodeCombiner.Add(Description);
87100
hashCodeCombiner.Add(Kind);
88101

89102
return hashCodeCombiner.CombinedHash;

0 commit comments

Comments
 (0)