提交 3df54594 编写于 作者: L lorcanmooney 提交者: Sam Harwell

Feedback from CyrusNajmabadi

上级 accff1e2
......@@ -1203,7 +1203,26 @@ public class Inner<TInner>
await VerifyItemsExistAsync(text, "TOuter", "TInner", "TMethod");
}
[Fact, Trait(Traits.Feature, Traits.Features.Completion)]
[WorkItem(17872, "https://github.com/dotnet/roslyn/issues/17872")]
[Fact, Trait(Traits.Feature, Traits.Features.Completion)]
public async Task TypeParamRefNamesPartiallyTyped()
{
var text = @"
public class Outer<TOuter>
{
public class Inner<TInner>
{
/// <summary>
/// <typeparamref name=""T$$""/>
/// </summary>
public int Method<TMethod>(T green) { }
}
}";
await VerifyItemsExistAsync(text, "TOuter", "TInner", "TMethod");
}
[Fact, Trait(Traits.Feature, Traits.Features.Completion)]
public async Task TypeParamNames()
{
var text = @"
......@@ -1222,6 +1241,25 @@ public class Inner<TInner>
await VerifyItemsAbsentAsync(text, "TOuter", "TInner");
}
[Fact, Trait(Traits.Feature, Traits.Features.Completion)]
public async Task TypeParamNamesPartiallyTyped()
{
var text = @"
public class Outer<TOuter>
{
public class Inner<TInner>
{
/// <summary>
/// <typeparam name=""T$$""/>
/// </summary>
public int Method<TMethod>(T green) { }
}
}";
await VerifyItemsExistAsync(text, "TMethod");
await VerifyItemsAbsentAsync(text, "TOuter", "TInner");
}
[WorkItem(8322, "https://github.com/dotnet/roslyn/issues/8322")]
[Fact, Trait(Traits.Feature, Traits.Features.Completion)]
public async Task PartialTagCompletion()
......
......@@ -818,6 +818,19 @@ End Class
Await VerifyItemsAbsentAsync(text, "TClass", "i")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.Completion)>
Public Async Function TestTypeParamNamesPartiallyTyped() As Task
Dim text = "
Class C(Of TClass)
''' <typeparam name=""T$$""
Sub Goo(Of TMethod)(i as TMethod)
End Sub
End Class
"
Await VerifyItemsExistAsync(text, "TMethod")
Await VerifyItemsAbsentAsync(text, "TClass", "i")
End Function
<WorkItem(17872, "https://github.com/dotnet/roslyn/issues/17872")>
<Fact, Trait(Traits.Feature, Traits.Features.Completion)>
Public Async Function TestTypeParamRefNames() As Task
......@@ -831,6 +844,24 @@ Class Outer(Of TOuter)
End Sub
End Class
End Class
"
Await VerifyItemsExistAsync(text, "TOuter", "TInner", "TMethod")
Await VerifyItemsAbsentAsync(text, "i")
End Function
<WorkItem(17872, "https://github.com/dotnet/roslyn/issues/17872")>
<Fact, Trait(Traits.Feature, Traits.Features.Completion)>
Public Async Function TestTypeParamRefNamesPartiallyTyped() As Task
Dim text = "
Class Outer(Of TOuter)
Class Inner(Of TInner)
''' <summary>
''' <typeparamref name=""T$$""
''' </summary>
Sub Goo(Of TMethod)(i as Integer)
End Sub
End Class
End Class
"
Await VerifyItemsExistAsync(text, "TOuter", "TInner", "TMethod")
Await VerifyItemsAbsentAsync(text, "i")
......
......@@ -62,21 +62,28 @@ internal override bool IsInsertionTrigger(SourceText text, int characterPosition
}
}
string elementName, attributeName;
if (!(trigger.Kind == CompletionTriggerKind.Insertion && trigger.Character == ' ') &&
IsAttributeValueContext(token, out elementName, out attributeName))
if (IsAttributeNameContext(token, position, out string elementName, out ISet<string> existingAttributes))
{
return GetAttributeValueItems(declaredSymbol, elementName, attributeName);
return GetAttributeItems(elementName, existingAttributes);
}
ISet<string> existingAttributes;
if (IsAttributeNameContext(token, position, out elementName, out existingAttributes))
var wasTriggeredAfterSpace = trigger.Kind == CompletionTriggerKind.Insertion && trigger.Character == ' ';
if (wasTriggeredAfterSpace)
{
return GetAttributeItems(elementName, existingAttributes);
// Nothing below this point should triggered by a space character
// (only attribute names should be triggered by <SPACE>)
return null;
}
if (IsAttributeValueContext(token, out elementName, out string attributeName))
{
return GetAttributeValueItems(declaredSymbol, elementName, attributeName);
}
if (trigger.Kind == CompletionTriggerKind.Insertion && trigger.Character != '<')
{
// With the use of IsTriggerAfterSpaceOrStartOfWordCharacter, the code below is much
// too aggressive at suggesting tags, so exit early before degrading the experience
return null;
}
......@@ -158,26 +165,27 @@ private bool IsAttributeNameContext(SyntaxToken token, int position, out string
// Handle the <elem$$ case by going back one token (the subsequent checks need to account for this)
token = token.GetPreviousTokenIfTouchingWord(position);
SyntaxList<XmlAttributeSyntax> attributes = default(SyntaxList<XmlAttributeSyntax>);
var attributes = default(SyntaxList<XmlAttributeSyntax>);
if (token.IsKind(SyntaxKind.IdentifierToken) && token.IsParentKind(SyntaxKind.XmlName))
if (token.IsKind(SyntaxKind.IdentifierToken) && token.Parent.IsKind(SyntaxKind.XmlName))
{
// <elem $$
// <elem attr$$
elementName = GetElementNameAndAttributes(token.Parent.Parent, out attributes);
(elementName, attributes) = GetElementNameAndAttributes(token.Parent.Parent);
}
else if (token.IsParentKind(SyntaxKind.XmlCrefAttribute) ||
token.IsParentKind(SyntaxKind.XmlNameAttribute) ||
token.IsParentKind(SyntaxKind.XmlTextAttribute))
else if (token.Parent.IsKind(SyntaxKind.XmlCrefAttribute) ||
token.Parent.IsKind(SyntaxKind.XmlNameAttribute) ||
token.Parent.IsKind(SyntaxKind.XmlTextAttribute))
{
// <elem attr="" $$
// <elem attr="" $$attr
// <elem attr="" attr$$
// In the following, 'attr1' may be a regular text attribute, or one of the special 'cref' or 'name' attributes
// <elem attr1="" $$
// <elem attr1="" $$attr2
// <elem attr1="" attr2$$
var attributeSyntax = (XmlAttributeSyntax)token.Parent;
if (token == attributeSyntax.EndQuoteToken)
{
elementName = GetElementNameAndAttributes(attributeSyntax.Parent, out attributes);
(elementName, attributes) = GetElementNameAndAttributes(attributeSyntax.Parent);
}
}
......@@ -185,24 +193,26 @@ private bool IsAttributeNameContext(SyntaxToken token, int position, out string
return elementName != null;
}
private string GetElementNameAndAttributes(SyntaxNode node, out SyntaxList<XmlAttributeSyntax> attributes)
private (string name, SyntaxList<XmlAttributeSyntax> attributes) GetElementNameAndAttributes(SyntaxNode node)
{
XmlNameSyntax nameSyntax;
SyntaxList<XmlAttributeSyntax> attributes;
switch (node.Kind())
switch (node)
{
case SyntaxKind.XmlEmptyElement:
var emptyElementSyntax = (XmlEmptyElementSyntax)node;
// Self contained empty element <tag />
case XmlEmptyElementSyntax emptyElementSyntax:
nameSyntax = emptyElementSyntax.Name;
attributes = emptyElementSyntax.Attributes;
break;
case SyntaxKind.XmlElement:
node = ((XmlElementSyntax)node).StartTag;
goto case SyntaxKind.XmlElementStartTag;
// Parent node of a non-empty element: <tag></tag>
case XmlElementSyntax elementSyntax:
// Defer to the start-tag logic
return GetElementNameAndAttributes(elementSyntax.StartTag);
case SyntaxKind.XmlElementStartTag:
var startTagSyntax = (XmlElementStartTagSyntax)node;
// Start tag of a non-empty element: <tag>
case XmlElementStartTagSyntax startTagSyntax:
nameSyntax = startTagSyntax.Name;
attributes = startTagSyntax.Attributes;
break;
......@@ -213,27 +223,28 @@ private string GetElementNameAndAttributes(SyntaxNode node, out SyntaxList<XmlAt
break;
}
return nameSyntax?.LocalName.ValueText;
return (name: nameSyntax?.LocalName.ValueText, attributes: attributes);
}
private bool IsAttributeValueContext(SyntaxToken token, out string tagName, out string attributeName)
{
XmlAttributeSyntax attributeSyntax = null;
if (token.IsParentKind(SyntaxKind.IdentifierName) && token.Parent.IsParentKind(SyntaxKind.XmlNameAttribute))
if (token.Parent.IsKind(SyntaxKind.IdentifierName) && token.Parent.IsParentKind(SyntaxKind.XmlNameAttribute))
{
// name="bar$$
// Handle the special 'name' attributes: name="bar$$
attributeSyntax = (XmlNameAttributeSyntax)token.Parent.Parent;
}
else if (token.IsKind(SyntaxKind.XmlTextLiteralToken) && token.IsParentKind(SyntaxKind.XmlTextAttribute))
else if (token.IsKind(SyntaxKind.XmlTextLiteralToken) && token.Parent.IsKind(SyntaxKind.XmlTextAttribute))
{
// foo="bar$$
// Handle the other general text attributes: foo="bar$$
attributeSyntax = (XmlTextAttributeSyntax)token.Parent;
}
else if (token.IsParentKind(SyntaxKind.XmlNameAttribute) || token.IsParentKind(SyntaxKind.XmlTextAttribute))
else if (token.Parent.IsKind(SyntaxKind.XmlNameAttribute) || token.Parent.IsKind(SyntaxKind.XmlTextAttribute))
{
// name="$$
// foo="$$
// When there's no attribute value yet, the parent attribute is returned:
// name="$$
// foo="$$
attributeSyntax = (XmlAttributeSyntax)token.Parent;
if (token != attributeSyntax.StartQuoteToken)
{
......@@ -241,7 +252,6 @@ private bool IsAttributeValueContext(SyntaxToken token, out string tagName, out
}
}
if (attributeSyntax != null)
{
attributeName = attributeSyntax.Name.LocalName.ValueText;
......@@ -249,17 +259,17 @@ private bool IsAttributeValueContext(SyntaxToken token, out string tagName, out
var emptyElement = attributeSyntax.GetAncestor<XmlEmptyElementSyntax>();
if (emptyElement != null)
{
// Empty element tags: <tag attr=... />
tagName = emptyElement.Name.LocalName.Text;
return true;
}
else
var startTagSyntax = token.GetAncestor<XmlElementStartTagSyntax>();
if (startTagSyntax != null)
{
var startTagSyntax = token.GetAncestor<XmlElementStartTagSyntax>();
if (startTagSyntax != null)
{
tagName = startTagSyntax.Name.LocalName.Text;
return true;
}
// Non-empty element start tags: <tag attr=... >
tagName = startTagSyntax.Name.LocalName.Text;
return true;
}
}
......@@ -268,15 +278,11 @@ private bool IsAttributeValueContext(SyntaxToken token, out string tagName, out
return false;
}
protected override IEnumerable<string> GetKeywordNames()
{
return SyntaxFacts.GetKeywordKinds().Select(SyntaxFacts.GetText);
}
protected override IEnumerable<string> GetKeywordNames() =>
SyntaxFacts.GetKeywordKinds().Select(SyntaxFacts.GetText);
protected override IEnumerable<string> GetExistingTopLevelElementNames(DocumentationCommentTriviaSyntax syntax)
{
return syntax.Content.Select(GetElementName);
}
protected override IEnumerable<string> GetExistingTopLevelElementNames(DocumentationCommentTriviaSyntax syntax) =>
syntax.Content.Select(GetElementName);
protected override IEnumerable<string> GetExistingTopLevelAttributeValues(DocumentationCommentTriviaSyntax syntax, string elementName, string attributeName)
{
......@@ -284,8 +290,9 @@ protected override IEnumerable<string> GetExistingTopLevelAttributeValues(Docume
foreach (var node in syntax.Content)
{
SyntaxList<XmlAttributeSyntax> attributes;
if (GetElementNameAndAttributes(node, out attributes) == elementName)
(var name, var attributes) = GetElementNameAndAttributes(node);
if (name == elementName)
{
attributeValues = attributeValues.Concat(
attributes.Where(attribute => GetAttributeName(attribute) == attributeName)
......@@ -296,22 +303,16 @@ protected override IEnumerable<string> GetExistingTopLevelAttributeValues(Docume
return attributeValues;
}
private string GetElementName(XmlNodeSyntax node)
{
SyntaxList<XmlAttributeSyntax> attributes;
return GetElementNameAndAttributes(node, out attributes);
}
private string GetElementName(XmlNodeSyntax node) => GetElementNameAndAttributes(node).name;
private string GetAttributeName(XmlAttributeSyntax attribute)
{
return attribute.Name.LocalName.ValueText;
}
private string GetAttributeName(XmlAttributeSyntax attribute) => attribute.Name.LocalName.ValueText;
private string GetAttributeValue(XmlAttributeSyntax attribute)
{
switch (attribute)
{
case XmlTextAttributeSyntax textAttribute:
// Decode any XML enities and concatentate the results
return textAttribute.TextTokens.GetValueText();
case XmlNameAttributeSyntax nameAttribute:
......
......@@ -24,19 +24,19 @@ internal abstract class AbstractDocCommentCompletionProvider<TSyntax> : CommonCo
private static readonly ImmutableArray<string> s_topLevelRepeatableTagNames = ImmutableArray.Create(ExceptionElementName, IncludeElementName, PermissionElementName);
private static readonly ImmutableArray<string> s_topLevelSingleUseTagNames = ImmutableArray.Create(SummaryElementName, RemarksElementName, ExampleElementName, CompletionListElementName);
private static readonly Dictionary<string, string[]> s_tagMap =
new Dictionary<string, string[]>
private static readonly Dictionary<string, (string tagOpen, string textBeforeCaret, string textAfterCaret, string tagClose)> s_tagMap =
new Dictionary<string, (string tagOpen, string textBeforeCaret, string textAfterCaret, string tagClose)>
{
// Open Before caret $$ After caret Close
{ ExceptionElementName, new[] { $"<{ExceptionElementName}", $" {CrefAttributeName}=\"", "\"", null } },
{ IncludeElementName, new[] { $"<{IncludeElementName}", $" {FileAttributeName}=\'", $"\' {PathAttributeName}=\'[@name=\"\"]\'", "/>" } },
{ PermissionElementName, new[] { $"<{PermissionElementName}", $" {CrefAttributeName}=\"", "\"", null } },
{ SeeElementName, new[] { $"<{SeeElementName}", $" {CrefAttributeName}=\"", "\"", "/>" } },
{ SeeAlsoElementName, new[] { $"<{SeeAlsoElementName}", $" {CrefAttributeName}=\"", "\"", "/>" } },
{ ListElementName, new[] { $"<{ListElementName}", $" {TypeAttributeName}=\"", "\"", null } },
{ ParameterReferenceElementName, new[] { $"<{ParameterReferenceElementName}", $" {NameAttributeName}=\"", "\"", "/>" } },
{ TypeParameterReferenceElementName, new[] { $"<{TypeParameterReferenceElementName}", $" {NameAttributeName}=\"", "\"", "/>" } },
{ CompletionListElementName, new[] { $"<{CompletionListElementName}", $" {CrefAttributeName}=\"", "\"", "/>" } },
// tagOpen textBeforeCaret $$ textAfterCaret tagClose
{ ExceptionElementName, ($"<{ExceptionElementName}", $" {CrefAttributeName}=\"", "\"", null) },
{ IncludeElementName, ($"<{IncludeElementName}", $" {FileAttributeName}=\'", $"\' {PathAttributeName}=\'[@name=\"\"]\'", "/>") },
{ PermissionElementName, ($"<{PermissionElementName}", $" {CrefAttributeName}=\"", "\"", null) },
{ SeeElementName, ($"<{SeeElementName}", $" {CrefAttributeName}=\"", "\"", "/>") },
{ SeeAlsoElementName, ($"<{SeeAlsoElementName}", $" {CrefAttributeName}=\"", "\"", "/>") },
{ ListElementName, ($"<{ListElementName}", $" {TypeAttributeName}=\"", "\"", null) },
{ ParameterReferenceElementName, ($"<{ParameterReferenceElementName}", $" {NameAttributeName}=\"", "\"", "/>") },
{ TypeParameterReferenceElementName, ($"<{TypeParameterReferenceElementName}", $" {NameAttributeName}=\"", "\"", "/>") },
{ CompletionListElementName, ($"<{CompletionListElementName}", $" {CrefAttributeName}=\"", "\"", "/>") },
};
private static readonly string[][] s_attributeMap =
......@@ -80,20 +80,18 @@ public override async Task ProvideCompletionsAsync(CompletionContext context)
protected abstract IEnumerable<string> GetExistingTopLevelAttributeValues(TSyntax syntax, string tagName, string attributeName);
private bool HasExistingTopLevelElement(TSyntax syntax, string name)
{
return GetExistingTopLevelElementNames(syntax).Contains(name);
}
private bool HasExistingTopLevelElement(TSyntax syntax, string name) =>
GetExistingTopLevelElementNames(syntax).Contains(name);
private CompletionItem GetItem(string name)
{
if (s_tagMap.TryGetValue(name, out var values))
{
return CreateCompletionItem(name,
beforeCaretText: values[0] + values[1],
afterCaretText: values[2] + values[3],
beforeCaretTextOnSpace: values[0],
afterCaretTextOnSpace: values[3]);
beforeCaretText: values.tagOpen + values.textBeforeCaret,
afterCaretText: values.textAfterCaret + values.tagClose,
beforeCaretTextOnSpace: values.tagOpen,
afterCaretTextOnSpace: values.tagClose);
}
return CreateCompletionItem(name);
......
......@@ -13,9 +13,12 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers
Inherits AbstractDocCommentCompletionProvider(Of DocumentationCommentTriviaSyntax)
Friend Overrides Function IsInsertionTrigger(text As SourceText, characterPosition As Integer, options As OptionSet) As Boolean
Return text(characterPosition) = "<"c OrElse text(characterPosition) = """"c OrElse
(text(characterPosition) = "/"c AndAlso characterPosition > 0 AndAlso text(characterPosition - 1) = "<"c) OrElse
IsTriggerAfterSpaceOrStartOfWordCharacter(text, characterPosition, options)
Dim isStartOfTag = text(characterPosition) = "<"c
Dim isClosingTag = (text(characterPosition) = "/"c AndAlso characterPosition > 0 AndAlso text(characterPosition - 1) = "<"c)
Dim isDoubleQuote = text(characterPosition) = """"c
Return isStartOfTag OrElse isClosingTag OrElse isDoubleQuote OrElse
IsTriggerAfterSpaceOrStartOfWordCharacter(text, characterPosition, options)
End Function
Public Function GetPreviousTokenIfTouchingText(token As SyntaxToken, position As Integer) As SyntaxToken
......@@ -88,7 +91,11 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers
End If
End If
If trigger.Kind = CompletionTriggerKind.Insertion AndAlso Not trigger.Character = """"c AndAlso Not trigger.Character = "<"c Then
If trigger.Kind = CompletionTriggerKind.Insertion AndAlso
Not trigger.Character = """"c AndAlso
Not trigger.Character = "<"c Then
' With the use of IsTriggerAfterSpaceOrStartOfWordCharacter, the code below is much
' too aggressive at suggesting tags, so exit early before degrading the experience
Return items
End If
......@@ -277,6 +284,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers
Private Function GetAttributeValue(attribute As XmlNodeSyntax) As String
If TypeOf attribute Is XmlAttributeSyntax Then
' Decode any XML enities and concatentate the results
Return DirectCast(DirectCast(attribute, XmlAttributeSyntax).Value, XmlStringSyntax).TextTokens.GetValueText()
End If
......
......@@ -43,11 +43,6 @@ public static bool IsKind(this SyntaxToken token, params SyntaxKind[] kinds)
return kinds.Contains(token.Kind());
}
public static bool IsParentKind(this SyntaxToken token, SyntaxKind kind)
{
return token.Parent != null && token.Parent.IsKind(kind);
}
public static bool IsLiteral(this SyntaxToken token)
{
switch (token.Kind())
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册