From a4fb66a3344e773d99671280f779370063090662 Mon Sep 17 00:00:00 2001 From: Jason Malinowski Date: Thu, 28 May 2020 15:45:01 -0700 Subject: [PATCH] Move the Quick Info handling for await back into Quick Info The Quick Info service mostly defers to the symbol description service to do most of it's work; the symbol description service takes a symbol and a SemanticModel + position to build up the description of the symbol, using that position for minimally qualifying types in some cases. This position was also being used by the symbol description service to see if the original invocation point was an await, and if so it switched to special behavior to give a different message entirely; there was some logic in Quick Info itself to also accomodate that. This moves that special handling back up to Quick Info, and also does a bit of a refactoring to include all of the decisions for what Quick Info will show into a struct. This will be used later to help the LSIF tool because it means if the two structs are equal than we know the two Quick Info contents would the same and we can reuse it. --- .../QuickInfo/SemanticQuickInfoSourceTests.cs | 6 +- .../QuickInfo/SemanticQuickInfoSourceTests.vb | 2 +- ...ervice.AbstractSymbolDescriptionBuilder.cs | 41 +-------- ...mmonSemanticQuickInfoProvider.TokenInfo.cs | 30 ++++++ .../CommonSemanticQuickInfoProvider.cs | 91 ++++++++++++------- .../VisualBasicSemanticQuickInfoProvider.vb | 12 +-- 6 files changed, 99 insertions(+), 83 deletions(-) create mode 100644 src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.TokenInfo.cs diff --git a/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs b/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs index a3d83cd8c6e..0518d806ad4 100644 --- a/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs +++ b/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs @@ -1345,7 +1345,7 @@ async Task UseAsync() result = await lambda(); } }"; - await TestAsync(markup, MainDescription($"({CSharpFeaturesResources.awaitable}) {string.Format(FeaturesResources.Awaited_task_returns_0, "class System.Threading.Tasks.Task")}"), + await TestAsync(markup, MainDescription(string.Format(FeaturesResources.Awaited_task_returns_0, $"({CSharpFeaturesResources.awaitable}) class System.Threading.Tasks.Task")), TypeParameterMap($"\r\nTResult {FeaturesResources.is_} int")); } @@ -5230,7 +5230,7 @@ async Task M() awa$$it M(); } }"; - await TestAsync(markup, MainDescription("int[]")); + await TestAsync(markup, MainDescription(string.Format(FeaturesResources.Awaited_task_returns_0, "int[]"))); } [WorkItem(1114300, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/1114300")] @@ -5247,7 +5247,7 @@ async Task M() awa$$it M(); } }"; - await TestAsync(markup, MainDescription("dynamic")); + await TestAsync(markup, MainDescription(string.Format(FeaturesResources.Awaited_task_returns_0, "dynamic"))); } [Fact, Trait(Traits.Feature, Traits.Features.Completion)] diff --git a/src/EditorFeatures/VisualBasicTest/QuickInfo/SemanticQuickInfoSourceTests.vb b/src/EditorFeatures/VisualBasicTest/QuickInfo/SemanticQuickInfoSourceTests.vb index 40207854b15..f25ff75d024 100644 --- a/src/EditorFeatures/VisualBasicTest/QuickInfo/SemanticQuickInfoSourceTests.vb +++ b/src/EditorFeatures/VisualBasicTest/QuickInfo/SemanticQuickInfoSourceTests.vb @@ -1897,7 +1897,7 @@ End Class .ToString() - Dim description = <<%= VBFeaturesResources.Awaitable %>> <%= String.Format(FeaturesResources.Awaited_task_returns_0, "Class System.Threading.Tasks.Task(Of TResult)") %>.ConvertTestSourceTag() + Dim description = <%= String.Format(FeaturesResources.Awaited_task_returns_0, $"<{VBFeaturesResources.Awaitable}> Class System.Threading.Tasks.Task(Of TResult)") %>.ConvertTestSourceTag() Await TestFromXmlAsync(markup, MainDescription(description), TypeParameterMap(vbCrLf & $"TResult {FeaturesResources.is_} Integer")) End Function diff --git a/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/AbstractSymbolDisplayService.AbstractSymbolDescriptionBuilder.cs b/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/AbstractSymbolDisplayService.AbstractSymbolDescriptionBuilder.cs index 2570282fa9e..f4d895df686 100644 --- a/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/AbstractSymbolDisplayService.AbstractSymbolDescriptionBuilder.cs +++ b/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/AbstractSymbolDisplayService.AbstractSymbolDescriptionBuilder.cs @@ -283,7 +283,7 @@ private async Task AddDescriptionPartAsync(ISymbol symbol) } else { - await AddDescriptionForNamedTypeAsync(namedType).ConfigureAwait(false); + AddDescriptionForNamedType(namedType); } } else if (symbol is INamespaceSymbol namespaceSymbol) @@ -380,37 +380,14 @@ private void AddDescriptionForDynamicType() PlainText(FeaturesResources.Represents_an_object_whose_operations_will_be_resolved_at_runtime)); } - private async Task AddDescriptionForNamedTypeAsync(INamedTypeSymbol symbol) + private void AddDescriptionForNamedType(INamedTypeSymbol symbol) { if (symbol.IsAwaitableNonDynamic(_semanticModel, _position)) { AddAwaitablePrefix(); } - var token = await _semanticModel.SyntaxTree.GetTouchingTokenAsync(_position, CancellationToken).ConfigureAwait(false); - if (token != default) - { - var syntaxFactsService = Workspace.Services.GetLanguageServices(token.Language).GetService(); - if (syntaxFactsService.IsAwaitKeyword(token)) - { - if (symbol.SpecialType == SpecialType.System_Void) - { - AddToGroup(SymbolDescriptionGroups.MainDescription, - PlainText(FeaturesResources.Awaited_task_returns_no_value)); - return; - } - - AddAwaitSymbolDescription(symbol); - } - else - { - AddSymbolDescription(symbol); - } - } - else - { - AddSymbolDescription(symbol); - } + AddSymbolDescription(symbol); if (!symbol.IsUnboundGenericType && !TypeArgumentsAndParametersAreSame(symbol)) { @@ -421,18 +398,6 @@ private async Task AddDescriptionForNamedTypeAsync(INamedTypeSymbol symbol) } } - private void AddAwaitSymbolDescription(INamedTypeSymbol symbol) - { - var defaultSymbol = "{0}"; - var symbolIndex = FeaturesResources.Awaited_task_returns_0.IndexOf(defaultSymbol); - - AddToGroup(SymbolDescriptionGroups.MainDescription, - PlainText(FeaturesResources.Awaited_task_returns_0.Substring(0, symbolIndex))); - AddSymbolDescription(symbol); - AddToGroup(SymbolDescriptionGroups.MainDescription, - PlainText(FeaturesResources.Awaited_task_returns_0.Substring(symbolIndex + defaultSymbol.Length))); - } - private void AddSymbolDescription(INamedTypeSymbol symbol) { if (symbol.TypeKind == TypeKind.Delegate) diff --git a/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.TokenInfo.cs b/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.TokenInfo.cs new file mode 100644 index 00000000000..a871113503b --- /dev/null +++ b/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.TokenInfo.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable + +using System.Collections.Immutable; + +namespace Microsoft.CodeAnalysis.QuickInfo +{ + internal abstract partial class CommonSemanticQuickInfoProvider + { + public struct TokenInformation + { + public readonly ImmutableArray Symbols; + + /// + /// True if this quick info came from hovering over an 'await' keyword, which we show the return + /// type of with special text. + /// + public readonly bool ShowAwaitReturn; + + public TokenInformation(ImmutableArray symbols, bool showAwaitReturn = false) + { + Symbols = symbols; + ShowAwaitReturn = showAwaitReturn; + } + } + } +} diff --git a/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs b/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs index b21f6ee0852..6ce3ea578df 100644 --- a/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs +++ b/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs @@ -26,19 +26,19 @@ internal abstract partial class CommonSemanticQuickInfoProvider : CommonQuickInf SyntaxToken token, CancellationToken cancellationToken) { - var (model, symbols, supportedPlatforms) = await ComputeQuickInfoDataAsync(document, token, cancellationToken).ConfigureAwait(false); + var (model, tokenInformation, supportedPlatforms) = await ComputeQuickInfoDataAsync(document, token, cancellationToken).ConfigureAwait(false); - if (symbols.IsDefaultOrEmpty) + if (tokenInformation.Symbols.IsDefaultOrEmpty) { return null; } return await CreateContentAsync(document.Project.Solution.Workspace, - token, model, symbols, supportedPlatforms, + token, model, tokenInformation, supportedPlatforms, cancellationToken).ConfigureAwait(false); } - private async Task<(SemanticModel model, ImmutableArray symbols, SupportedPlatformData? supportedPlatforms)> ComputeQuickInfoDataAsync( + private async Task<(SemanticModel model, TokenInformation tokenInformation, SupportedPlatformData? supportedPlatforms)> ComputeQuickInfoDataAsync( Document document, SyntaxToken token, CancellationToken cancellationToken) @@ -49,12 +49,12 @@ internal abstract partial class CommonSemanticQuickInfoProvider : CommonQuickInf return await ComputeFromLinkedDocumentsAsync(document, linkedDocumentIds, token, cancellationToken).ConfigureAwait(false); } - var (model, symbols) = await BindTokenAsync(document, token, cancellationToken).ConfigureAwait(false); + var (model, tokenInformation) = await BindTokenAsync(document, token, cancellationToken).ConfigureAwait(false); - return (model, symbols, supportedPlatforms: null); + return (model, tokenInformation, supportedPlatforms: null); } - private async Task<(SemanticModel model, ImmutableArray symbols, SupportedPlatformData supportedPlatforms)> ComputeFromLinkedDocumentsAsync( + private async Task<(SemanticModel model, TokenInformation, SupportedPlatformData supportedPlatforms)> ComputeFromLinkedDocumentsAsync( Document document, ImmutableArray linkedDocumentIds, SyntaxToken token, @@ -70,14 +70,14 @@ internal abstract partial class CommonSemanticQuickInfoProvider : CommonQuickInf // Instead, we need to find the head in which we get the best binding, // which in this case is the one with no errors. - var (model, symbols) = await BindTokenAsync(document, token, cancellationToken).ConfigureAwait(false); + var (model, tokenInformation) = await BindTokenAsync(document, token, cancellationToken).ConfigureAwait(false); var candidateProjects = new List() { document.Project.Id }; var invalidProjects = new List(); - var candidateResults = new List<(DocumentId docId, SemanticModel model, ImmutableArray symbols)> + var candidateResults = new List<(DocumentId docId, SemanticModel model, TokenInformation tokenInformation)> { - (document.Id, model, symbols) + (document.Id, model, tokenInformation) }; foreach (var linkedDocumentId in linkedDocumentIds) @@ -96,10 +96,10 @@ internal abstract partial class CommonSemanticQuickInfoProvider : CommonQuickInf // Take the first result with no errors. // If every file binds with errors, take the first candidate, which is from the current file. - var bestBinding = candidateResults.FirstOrNull(c => HasNoErrors(c.symbols)) + var bestBinding = candidateResults.FirstOrNull(c => HasNoErrors(c.tokenInformation.Symbols)) ?? candidateResults.First(); - if (bestBinding.symbols.IsDefaultOrEmpty) + if (bestBinding.tokenInformation.Symbols.IsDefaultOrEmpty) { return default; } @@ -109,7 +109,7 @@ internal abstract partial class CommonSemanticQuickInfoProvider : CommonQuickInf foreach (var candidate in candidateResults) { // Does the candidate have anything remotely equivalent? - if (!candidate.symbols.Intersect(bestBinding.symbols, LinkedFilesSymbolEquivalenceComparer.Instance).Any()) + if (!candidate.tokenInformation.Symbols.Intersect(bestBinding.tokenInformation.Symbols, LinkedFilesSymbolEquivalenceComparer.Instance).Any()) { invalidProjects.Add(candidate.docId.ProjectId); } @@ -117,7 +117,7 @@ internal abstract partial class CommonSemanticQuickInfoProvider : CommonQuickInf var supportedPlatforms = new SupportedPlatformData(invalidProjects, candidateProjects, document.Project.Solution.Workspace); - return (bestBinding.model, bestBinding.symbols, supportedPlatforms); + return (bestBinding.model, bestBinding.tokenInformation, supportedPlatforms); } private static bool HasNoErrors(ImmutableArray symbols) @@ -152,17 +152,17 @@ private static bool HasNoErrors(ImmutableArray symbols) Workspace workspace, SyntaxToken token, SemanticModel semanticModel, - IEnumerable symbols, + TokenInformation tokenInformation, SupportedPlatformData? supportedPlatforms, CancellationToken cancellationToken) { var descriptionService = workspace.Services.GetLanguageServices(token.Language).GetRequiredService(); var formatter = workspace.Services.GetLanguageServices(semanticModel.Language).GetRequiredService(); var syntaxFactsService = workspace.Services.GetLanguageServices(semanticModel.Language).GetRequiredService(); + var showWarningGlyph = supportedPlatforms != null && supportedPlatforms.HasValidAndInvalidProjects(); - var showSymbolGlyph = true; - var groups = await descriptionService.ToDescriptionGroupsAsync(workspace, semanticModel, token.SpanStart, symbols.AsImmutable(), cancellationToken).ConfigureAwait(false); + var groups = await descriptionService.ToDescriptionGroupsAsync(workspace, semanticModel, token.SpanStart, tokenInformation.Symbols, cancellationToken).ConfigureAwait(false); bool TryGetGroupText(SymbolDescriptionGroups group, out ImmutableArray taggedParts) => groups.TryGetValue(group, out taggedParts) && !taggedParts.IsDefaultOrEmpty; @@ -172,12 +172,42 @@ bool TryGetGroupText(SymbolDescriptionGroups group, out ImmutableArray taggedParts) => sections.Add(QuickInfoSection.Create(kind, taggedParts)); - if (TryGetGroupText(SymbolDescriptionGroups.MainDescription, out var mainDescriptionTaggedParts)) + if (tokenInformation.ShowAwaitReturn) { - AddSection(QuickInfoSectionKinds.Description, mainDescriptionTaggedParts); + // We show a special message if the Task being awaited has no return + if ((tokenInformation.Symbols.First() as INamedTypeSymbol)?.SpecialType == SpecialType.System_Void) + { + var builder = ImmutableArray.CreateBuilder(); + builder.AddText(FeaturesResources.Awaited_task_returns_no_value); + AddSection(QuickInfoSectionKinds.Description, builder.ToImmutable()); + return QuickInfoItem.Create(token.Span, sections: sections.ToImmutable()); + } + else + { + if (TryGetGroupText(SymbolDescriptionGroups.MainDescription, out var mainDescriptionTaggedParts)) + { + // We'll take the existing message and wrap it with a message saying this was returned from the task. + var defaultSymbol = "{0}"; + var symbolIndex = FeaturesResources.Awaited_task_returns_0.IndexOf(defaultSymbol); + + var builder = ImmutableArray.CreateBuilder(); + builder.AddText(FeaturesResources.Awaited_task_returns_0.Substring(0, symbolIndex)); + builder.AddRange(mainDescriptionTaggedParts); + builder.AddText(FeaturesResources.Awaited_task_returns_0.Substring(symbolIndex + defaultSymbol.Length)); + + AddSection(QuickInfoSectionKinds.Description, builder.ToImmutable()); + } + } + } + else + { + if (TryGetGroupText(SymbolDescriptionGroups.MainDescription, out var mainDescriptionTaggedParts)) + { + AddSection(QuickInfoSectionKinds.Description, mainDescriptionTaggedParts); + } } - var documentedSymbol = symbols.FirstOrDefault(); + var documentedSymbol = tokenInformation.Symbols.First(); // if generating quick info for an attribute, bind to the class instead of the constructor if (syntaxFactsService.IsAttributeName(token.Parent) && @@ -187,12 +217,6 @@ void AddSection(string kind, ImmutableArray taggedParts) } var documentationContent = GetDocumentationContent(documentedSymbol, groups, semanticModel, token, formatter, cancellationToken); - if (syntaxFactsService.IsAwaitKeyword(token) && - (symbols.First() as INamedTypeSymbol)?.SpecialType == SpecialType.System_Void) - { - documentationContent = default; - showSymbolGlyph = false; - } if (!documentationContent.IsDefaultOrEmpty) { @@ -286,11 +310,7 @@ void AddSection(string kind, ImmutableArray taggedParts) AddSection(QuickInfoSectionKinds.Captures, capturesText); } - var tags = ImmutableArray.Empty; - if (showSymbolGlyph) - { - tags = tags.AddRange(GlyphTags.GetTags(symbols.First().GetGlyph())); - } + var tags = ImmutableArray.CreateRange(GlyphTags.GetTags(tokenInformation.Symbols.First().GetGlyph())); if (showWarningGlyph) { @@ -408,10 +428,11 @@ void AddSection(string kind, ImmutableArray taggedParts) protected virtual ImmutableArray TryGetNullabilityAnalysis(Workspace workspace, SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) => default; - private async Task<(SemanticModel semanticModel, ImmutableArray symbols)> BindTokenAsync( + private async Task<(SemanticModel semanticModel, TokenInformation tokenInformation)> BindTokenAsync( Document document, SyntaxToken token, CancellationToken cancellationToken) { var syntaxFacts = document.GetRequiredLanguageService(); + var isAwait = syntaxFacts.IsAwaitKeyword(token); var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var enclosingType = semanticModel.GetEnclosingNamedType(token.SpanStart, cancellationToken); @@ -431,7 +452,7 @@ void AddSection(string kind, ImmutableArray taggedParts) if (symbols.Any()) { var discardSymbols = (symbols.First() as ITypeParameterSymbol)?.TypeParameterKind == TypeParameterKind.Cref; - return (semanticModel, discardSymbols ? ImmutableArray.Empty : symbols); + return (semanticModel, new TokenInformation(discardSymbols ? ImmutableArray.Empty : symbols, isAwait)); } // Couldn't bind the token to specific symbols. If it's an operator, see if we can at @@ -441,11 +462,11 @@ void AddSection(string kind, ImmutableArray taggedParts) var typeInfo = semanticModel.GetTypeInfo(token.Parent!, cancellationToken); if (IsOk(typeInfo.Type)) { - return (semanticModel, ImmutableArray.Create(typeInfo.Type)); + return (semanticModel, new TokenInformation(ImmutableArray.Create(typeInfo.Type))); } } - return (semanticModel, ImmutableArray.Empty); + return (semanticModel, new TokenInformation(ImmutableArray.Empty)); } private ImmutableArray GetSymbolsFromToken(SyntaxToken token, Workspace workspace, SemanticModel semanticModel, CancellationToken cancellationToken) diff --git a/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicSemanticQuickInfoProvider.vb b/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicSemanticQuickInfoProvider.vb index d1e34a17069..04a8463f960 100644 --- a/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicSemanticQuickInfoProvider.vb +++ b/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicSemanticQuickInfoProvider.vb @@ -121,7 +121,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.QuickInfo Return False End Function - Private Overloads Async Function BuildContentAsync( + Private Overloads Shared Async Function BuildContentAsync( document As Document, token As SyntaxToken, declarators As SeparatedSyntaxList(Of VariableDeclaratorSyntax), @@ -134,7 +134,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.QuickInfo Dim semantics = Await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(False) Dim types = declarators.SelectMany(Function(d) d.Names).Select( - Function(n) + Function(n) As ISymbol Dim symbol = semantics.GetDeclaredSymbol(n, cancellationToken) If symbol Is Nothing Then Return Nothing @@ -147,17 +147,17 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.QuickInfo Else Return Nothing End If - End Function).WhereNotNull().Distinct().ToList() + End Function).WhereNotNull().Distinct().ToImmutableArray() - If types.Count = 0 Then + If types.Length = 0 Then Return Nothing End If - If types.Count > 1 Then + If types.Length > 1 Then Return QuickInfoItem.Create(token.Span, sections:=ImmutableArray.Create(QuickInfoSection.Create(QuickInfoSectionKinds.Description, ImmutableArray.Create(New TaggedText(TextTags.Text, VBFeaturesResources.Multiple_Types))))) End If - Return Await CreateContentAsync(document.Project.Solution.Workspace, token, semantics, types, supportedPlatforms:=Nothing, cancellationToken:=cancellationToken).ConfigureAwait(False) + Return Await CreateContentAsync(document.Project.Solution.Workspace, token, semantics, New TokenInformation(types), supportedPlatforms:=Nothing, cancellationToken:=cancellationToken).ConfigureAwait(False) End Function Private Shared Async Function BuildContentForIntrinsicOperatorAsync(document As Document, -- GitLab