-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
QuickInfo should show the captures for lambdas and local functions #23970
Conversation
This is awesome. just two suggestions: it would be nice if we underline function name and lambda arrow as a visual clue if there is any capture (so you don't need to hover over any function/lambda to make sure it's not capturing, for example). also, it might make sense to group parameters and locals (if that's helpful at all). #WontFix |
I think I'll need Ali's PR (#23428) to get scenarios with multiple capture sets to work ( Update: |
@jcouv I'm a big fan of this and a few related items. I've marked this as a work in progress (in a way the bot understands) and will follow up in the original issue. #Resolved |
@@ -587,6 +587,11 @@ public bool IsBindableToken(SyntaxToken token) | |||
return true; | |||
} | |||
|
|||
if (token.IsKind(SyntaxKind.EqualsGreaterThanToken)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did something comparable while adding support for QuickInfo for Linq query syntax here in PR #23049. I was wondering whether this might have unwanted side effects (In my case it is even worse, because it is a CommaToken). If you experience something unexpected it would be nice to get me informed. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say @MaStr11's concerns are valid here, but maybe it's fine...? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@[email protected] Can you clarify the concern?
The method is only used in GetSemanticInfoAtPositionAsync
which now supports finding a symbol when the position is in =>
.
GetSemanticInfoAtPositionAsync
itself is used in GoToDefinition
, Peek
and SymbolFinder
(which is used in numerous places, like FindUsages
, FindReferences
, ...).
In reply to: 161932149 [](ancestors = 161932149)
@jcouv I'll ignore the actual code for review until the WIP is removed, but this should probably target master and not be carried along with features/compiler. This is an IDE-only feature that can be shipped independent of new language features. #Resolved |
@jasonmalinowski This cannot go into master, as it depends on a new compiler API ( There are two test regressions, which I'm still investigating. When I ask for a semantic model for the some lambda syntaxes, I get back a Also, @sharwell asked for IDE team to discuss this. It's probably better to hold-off review until design is approved. #Resolved |
|
void local() { i++; this.M(); } | ||
} | ||
}", | ||
Captures($"\r\n{WorkspacesResources.Captures_colon} @this, i")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@this? #Closed
AssertTextAndClassifications(expectedText, expectedClassifications: null, actualContent: ((QuickInfoDisplayDeferredContent)content).CapturesText); | ||
}; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider removing all the curlies here. Catures() => content => ...
#Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Felt harder to visually parse. Also, it's inconsistent with neighboring code. #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, it's inconsistent with neighboring code.
Fix the neighboring code :)
(But if you feel like it's actually harder to parse, that's a totally legitimate reason to not do it). #Resolved
parts.AddRange(ToMinimalDisplayParts(captured, s_formatForCaptures)); | ||
first = false; | ||
} | ||
AddToGroup(SymbolDescriptionGroups.Captures, parts); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
newlines after } (above as well). #Resolved
public bool IdentifiesLambda(SyntaxToken token) | ||
{ | ||
return token.IsKind(SyntaxKind.EqualsGreaterThanToken); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: use => #Closed
@@ -341,6 +341,8 @@ internal interface ISyntaxFactsService : ILanguageService | |||
Location GetDeconstructionReferenceLocation(SyntaxNode node); | |||
|
|||
SyntaxToken? GetDeclarationIdentifierIfOverride(SyntaxToken token); | |||
|
|||
bool IdentifiesLambda(SyntaxToken token); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on the fence if this is the right location for this method. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm planning to leave as-is unless I hear some better suggestion ;-)
In reply to: 159457900 [](ancestors = 159457900)
allSymbols = semanticModel.GetSymbolInfo(bindableParent, cancellationToken) | ||
.GetBestOrAllSymbols() | ||
.WhereAsArray(s => !s.Equals(declaredSymbol)) | ||
.SelectAsArray(s => MapSymbol(s, type)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
align dots. #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM (with changes + vb support).
@@ -85,7 +85,7 @@ private static bool IsEscapable(SymbolDisplayPartKind kind) | |||
private static string EscapeIdentifier(string identifier) | |||
{ | |||
var kind = SyntaxFacts.GetKeywordKind(identifier); | |||
return kind == SyntaxKind.None | |||
return kind == SyntaxKind.None || kind == SyntaxKind.ThisKeyword |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks wrong. this means we would not properly escape things in something like Foo(int @this)
anymore. #Resolved
@@ -4934,18 +4954,18 @@ public static void Main() | |||
Assert.Empty(flowAnalysis.Captured); | |||
Assert.Empty(flowAnalysis.CapturedInside); | |||
Assert.Empty(flowAnalysis.CapturedOutside); | |||
Assert.Equal("MyClass @this", flowAnalysis.DataFlowsIn.Single().ToTestDisplayString()); | |||
Assert.Equal("MyClass this", flowAnalysis.DataFlowsIn.Single().ToTestDisplayString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup. these are all bad changes. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha! #Closed
var syntaxFacts = document.Project.LanguageServices.GetService<ISyntaxFactsService>(); | ||
|
||
ImmutableArray<ISymbol> symbols; | ||
if (syntaxFacts.IdentifiesLambda(token)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah. i would just have IdentifiesLambda just pulled into this type as an abstract method. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done :-) #Resolved
@@ -92,6 +92,13 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.QuickInfo | |||
Return Await MyBase.BuildContentAsync(document, token, cancellationToken).ConfigureAwait(False) | |||
End Function | |||
|
|||
''' <summary> | |||
''' Given a Sub or Function token for a lambda, returns the syntax for the whole lambda | |||
''' </summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we have tests for single/multiline sub/function lambdas? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I have all four cases #Resolved
Fixed a couple of test regressions. @dotnet/roslyn-ide for review. Thanks #Resolved |
@dotnet/roslyn-ide for review. All the feedback so far was addressed. Thanks #Resolved |
&& token.Parent.IsKind(SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.SimpleLambdaExpression)) | ||
{ | ||
// () => | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure how i feel about this style of no-body then falling out to common code :) #Resolved
''' If the token is a 'Sub' or 'Function' in a lambda, returns the syntax for the whole lambda | ||
''' </summary> | ||
Protected Overrides Function GetBindableNodeForTokenIndicatingLambda(token As SyntaxToken, <Out> ByRef found As SyntaxNode) As Boolean | ||
If token.IsKind(SyntaxKind.SubKeyword, SyntaxKind.FunctionKeyword) AndAlso token.Parent.IsKind(SyntaxKind.SubLambdaHeader, SyntaxKind.FunctionLambdaHeader) Then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to verify: this works for single and multi-line VB lambdas? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Here's what the syntax tree looks like (for Sub):
Also, I have tests for both multi-line and single-line:
<Fact, Trait(Traits.Feature, Traits.Features.QuickInfo)>
<WorkItem(23307, "https://github.com/dotnet/roslyn/issues/23307")>
Public Async Function TestQuickInfoCaptures() As Task
Await TestAsync("
Class C
Sub M(x As Integer)
Dim a As System.Action = Sub$$()
x = x + 1
End Sub
End Sub
End Class
",
Captures($"{vbCrLf}{WorkspacesResources.Variables_captured_colon} x"))
End Function
<Fact, Trait(Traits.Feature, Traits.Features.QuickInfo)>
<WorkItem(23307, "https://github.com/dotnet/roslyn/issues/23307")>
Public Async Function TestQuickInfoCaptures2() As Task
Await TestAsync("
Class C
Sub M(x As Integer)
Dim a As System.Action = S$$ub() x = x + 1
End Sub
End Class
",
Captures($"{vbCrLf}{WorkspacesResources.Variables_captured_colon} x"))
End Function
``` #Resolved
} | ||
} | ||
|
||
private async Task TestWithOptionsAsync(TestWorkspace workspace, params Action<object>[] expectedResults) | ||
private async Task TestWithOptionsAsync(TestWorkspace workspace, bool skipSpeculative, params Action<object>[] expectedResults) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm a little confused as to why we would still need this. Since quick-info is no longer using any speculative semantic models, there should be no effect of trying with/without speculation. Indeed, i would be fine if the CanUseSpeculativeSemanticModelAsync part of the test was removed entirely (though keeping it would be fine.) #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry about that. It was a left-over I should have cleaned up. Thanks #Resolved
var semanticModel = GetSemanticModel(syntax.SyntaxTree); | ||
if(semanticModel.IsSpeculativeSemanticModel) | ||
{ | ||
// In the context of symbol completion, it is possible we'll make a description for a symbol while speculating (but it won't be displayed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lolwut? we make a description, but don't display it?
Also, if that's the case, why add the group at all? why not just insta bail?
Finally, if you are going to check IsSpeculativeSemanticModel, it's worth calling that that's because data flow doens't work with SpeculativeSemanticModels, so you need to avoid the code below this check. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll clarify the comment. When you complete a local symbol, we make a description (with many parts: main description, exceptions, captures, etc), but only the main description seems to be displayed.
The reason I put captures here anyways, is in case I'm wrong and missed some scenario. Captures listing "?" is less ambiguous/misleading than displaying no captures at all.
Thanks! #Pending
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, but I'd expect that the this/Me captures in quick info are tagged as keywords, not parameters to be consistent with how the IDE would classify it. I'd expect that to be a one-line fix...somewhere?
@@ -266,16 +273,28 @@ internal abstract partial class AbstractSemanticQuickInfoProvider : AbstractQuic | |||
return CreateDocumentationCommentDeferredContent(null); | |||
} | |||
|
|||
protected abstract bool GetBindableNodeForTokenIndicatingLambda(SyntaxToken token, out SyntaxNode found); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To check: this isn't being used by TypeScript or F# that this is a breaking change to derivers? I don't see how they could , but want to confirm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked with F# folks (Will and Brett) and they do not reference AbstractSemanticQuickInfoProvider
from Roslyn (they only depend on some interfaces for quick info provider.
Same thing with TypeScript (Mine Starks), which only depends on interfaces, but not this abstract type.
} | ||
|
||
parts.AddRange(Space(count: 1)); | ||
parts.AddRange(ToMinimalDisplayParts(captured, s_formatForCaptures)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I'd expect the magic this parameter to be called a keyword, not a parameter. Our symbol model indeed calls it parameter; users don't.
@jasonmalinowski Good catch. I fixed the compiler tests to check part kinds too. @dotnet/roslyn-compiler for review. The compiler portion of this PR is a small change with how we display parts for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compiler changes LGTM (iteration 12)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compiler changes LGTM.
Love it when that happens! |
@VSadov raised a good question with nested scopes. Here's what I observe with nesting: But the outer local says that it captures |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Customer scenario
Performance-sensitive code should beware of captured variables when using lambdas. But it's not always easy to tell what variables are captured and will cause a closure to be created.
This feature adds a "Captures" section the QuickInfo bubble:
Bugs this fixes
Fixes #23307
Workarounds, if any
Figures out the captures mentally.
Risk
Performance impact
Is this a regression from a previous update?
No.
Note for the record: this was also implemented in monodevelop (mono/monodevelop#4569)